From 3c2cb28e84aa231d09bb8e5958b61eb2ef056eb2 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 1 Dec 2016 13:32:05 +0100 Subject: [PATCH 001/206] Added lighter count badge background-color for on white backgrounds --- app/assets/stylesheets/framework/nav.scss | 6 +++++- app/assets/stylesheets/pages/environments.scss | 2 +- app/assets/stylesheets/pages/pipelines.scss | 2 +- app/views/layouts/nav/_admin.html.haml | 2 +- app/views/layouts/nav/_group.html.haml | 4 ++-- app/views/layouts/nav/_project.html.haml | 4 ++-- changelogs/unreleased/badge-color-on-white-bg.yml | 4 ++++ 7 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/badge-color-on-white-bg.yml diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 1839ffa0976..40b696774a4 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -75,10 +75,14 @@ .badge { font-weight: normal; - background-color: #eee; + background-color: #f3f3f3; color: $btn-transparent-color; vertical-align: baseline; } + + .badge-dark { + background-color: #eee; + } } &.sub-nav { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 4b382e8adaf..658aec5e609 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -116,7 +116,7 @@ .badge { font-weight: normal; - background-color: $gray-darker; + background-color: #f3f3f3; color: $gl-placeholder-color; vertical-align: baseline; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0027d2caf22..651e82b7b45 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -507,7 +507,7 @@ } .badge { - background-color: $gray-darker; + background-color: #f3f3f3; color: $gl-text-color-light; font-weight: normal; margin-left: $btn-xs-side-margin; diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..b69114c96cc 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -31,7 +31,7 @@ = link_to admin_abuse_reports_path, title: "Abuse Reports" do %span Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f7edb47b666..c866767a2be 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -26,13 +26,13 @@ %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) + %span.badge.badge-dark.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(merge_requests.count) + %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 701bcd3ab71..a67a63adba8 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -70,14 +70,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + %span.badge.badge-dark.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml new file mode 100644 index 00000000000..680d7ff11f0 --- /dev/null +++ b/changelogs/unreleased/badge-color-on-white-bg.yml @@ -0,0 +1,4 @@ +--- +title: Added lighter count badge background-color for on white backgrounds +merge_request: 7873 +author: From af5de82c67bd1867e69438b71f67f43d026e4edf Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 2 Dec 2016 00:41:22 +0100 Subject: [PATCH 002/206] created framework for badges, solving duplicate code --- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/badges.scss | 10 ++++++++++ app/assets/stylesheets/framework/nav.scss | 11 ----------- app/assets/stylesheets/framework/variables.scss | 7 +++++++ app/assets/stylesheets/pages/environments.scss | 7 ------- app/assets/stylesheets/pages/pipelines.scss | 3 --- 6 files changed, 18 insertions(+), 21 deletions(-) create mode 100644 app/assets/stylesheets/framework/badges.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 7c7f991dd87..c26fea8d5b2 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -9,6 +9,7 @@ @import "framework/avatar.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; +@import "framework/badges.scss"; @import "framework/calendar.scss"; @import "framework/callout.scss"; @import "framework/common.scss"; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss new file mode 100644 index 00000000000..89e1c7890d8 --- /dev/null +++ b/app/assets/stylesheets/framework/badges.scss @@ -0,0 +1,10 @@ +.badge { + font-weight: normal; + background-color: $badge-bg; + color: $badge-color; + vertical-align: baseline; +} + +.badge-dark { + background-color: $badge-bg-dark; +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 40b696774a4..477582f9e3f 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -72,17 +72,6 @@ color: $black; font-weight: 600; } - - .badge { - font-weight: normal; - background-color: #f3f3f3; - color: $btn-transparent-color; - vertical-align: baseline; - } - - .badge-dark { - background-color: #eee; - } } &.sub-nav { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8a9c279d124..651e58d0bb4 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -247,6 +247,13 @@ $btn-placeholder-gray: #c7c7c7; $btn-white-active: #848484; $btn-gray-hover: #eee; +/* +* Badges +*/ +$badge-bg: #f3f3f3; +$badge-bg-dark: #eee; +$badge-color: $btn-transparent-color; + /* * Award emoji */ diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 658aec5e609..387d2818495 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -113,13 +113,6 @@ .folder-name { cursor: pointer; - - .badge { - font-weight: normal; - background-color: #f3f3f3; - color: $gl-placeholder-color; - vertical-align: baseline; - } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 651e82b7b45..90e933cbd61 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -507,9 +507,6 @@ } .badge { - background-color: #f3f3f3; - color: $gl-text-color-light; - font-weight: normal; margin-left: $btn-xs-side-margin; } } From 429c9220f0ce49fa54ea640cbfc78e3591202138 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 12 Dec 2016 09:31:48 +0100 Subject: [PATCH 003/206] Setup mattermost session --- lib/mattermost/mattermost.rb | 102 +++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ++++++++++ 2 files changed, 144 insertions(+) create mode 100644 lib/mattermost/mattermost.rb create mode 100644 spec/lib/mattermost/mattermost_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/mattermost.rb new file mode 100644 index 00000000000..84d016bb197 --- /dev/null +++ b/lib/mattermost/mattermost.rb @@ -0,0 +1,102 @@ +module Mattermost + class NoSessionError < StandardError; end + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Mattermost + include Doorkeeper::Helpers::Controller + include HTTParty + + attr_accessor :current_resource_owner + + def initialize(uri, current_user) + self.class.base_uri(uri) + + @current_resource_owner = current_user + end + + def with_session + raise NoSessionError unless create + yield + destroy + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys + end + + private + + def create + return unless oauth_uri + return unless token_uri + + self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") + + request_token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri ||= URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= if @oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + @request_token ||= if @token_uri + response = get(@token_uri, follow_redirects: false) + response.headers['token'] if 200 <= response.code && response.code < 400 + end + end + + def get(path, options = {}) + self.class.get(path, options) + end + + def post(path, options = {}) + self.class.post(path, options) + end + end +end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb new file mode 100644 index 00000000000..7c99b4df9f3 --- /dev/null +++ b/spec/lib/mattermost/mattermost_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Mattermost::Mattermost do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorman to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + end + end + end +end From 4b23764da7622ae6bc40697c7c623df901cae01b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 15 Dec 2016 14:32:50 +0100 Subject: [PATCH 004/206] Improve session tests --- lib/mattermost/{mattermost.rb => session.rb} | 9 ++- spec/lib/mattermost/mattermost_spec.rb | 42 ------------ spec/lib/mattermost/session_spec.rb | 68 ++++++++++++++++++++ 3 files changed, 74 insertions(+), 45 deletions(-) rename lib/mattermost/{mattermost.rb => session.rb} (95%) delete mode 100644 spec/lib/mattermost/mattermost_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/session.rb similarity index 95% rename from lib/mattermost/mattermost.rb rename to lib/mattermost/session.rb index 84d016bb197..d14121c91a0 100644 --- a/lib/mattermost/mattermost.rb +++ b/lib/mattermost/session.rb @@ -13,13 +13,14 @@ module Mattermost # This class however skips the button click, and also the approval phase to # speed up the process and keep it without manual action and get a session # going. - class Mattermost + class Session include Doorkeeper::Helpers::Controller include HTTParty attr_accessor :current_resource_owner def initialize(uri, current_user) + # Sets the base uri for HTTParty, so we can use paths self.class.base_uri(uri) @current_resource_owner = current_user @@ -27,8 +28,10 @@ module Mattermost def with_session raise NoSessionError unless create - yield + result = yield destroy + + result end # Next methods are needed for Doorkeeper @@ -85,7 +88,7 @@ module Mattermost end def request_token - @request_token ||= if @token_uri + @request_token ||= begin response = get(@token_uri, follow_redirects: false) response.headers['token'] if 200 <= response.code && response.code < 400 end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb deleted file mode 100644 index 7c99b4df9f3..00000000000 --- a/spec/lib/mattermost/mattermost_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Mattermost do - let(:user) { create(:user) } - - subject { described_class.new('http://localhost:8065', user) } - - # Needed for doorman to function - it { is_expected.to respond_to(:current_resource_owner) } - it { is_expected.to respond_to(:request) } - it { is_expected.to respond_to(:authorization) } - it { is_expected.to respond_to(:strategy) } - - describe '#with session' do - let!(:stub) do - WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). - to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) - end - - context 'without oauth uri' do - it 'makes a request to the oauth uri' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - - context 'with oauth_uri' do - let!(:doorkeeper) do - Doorkeeper::Application.create(name: "GitLab Mattermost", - redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", - scopes: "") - end - - context 'without token_uri' do - it 'can not create a session' do - expect do - subject.with_session - end.to raise_error(Mattermost::NoSessionError) - end - end - end - end - end -end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..a93bab877da --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Mattermost::Session do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "eyJhY3Rpb24iOiJsb2dpbiIsImhhc2giOiIkMmEkMTAkVC9wYVlEaTdIUS8vcWdKRmdOOUllZUptaUNJWUlvNVNtNEcwU2NBMXFqelNOVmVPZ1cxWUsifQ%3D%3D" } + let(:location) { "http://locahost:8065/oauth/authorize?response_type=code&client_id=#{doorkeeper.uid}&redirect_uri=http%3A%2F%2Flocalhost:8065%2Fsignup%2Fgitlab%2Fcomplete&state=#{state}" } + + before do + WebMock.stub_request(:get, /http:\/\/localhost:8065\/signup\/gitlab\/complete*/). + to_return(headers: { 'token' => 'thisworksnow' }, status: 202) + end + + it 'can setup a session' do + expect(subject).to receive(:destroy) + + subject.with_session { 1 + 1 } + end + + it 'returns the value of the block' do + WebMock.stub_request(:post, "http://localhost:8065/api/v3/users/logout"). + to_return(headers: { 'token' => 'thisworksnow' }, status: 200) + + value = subject.with_session { 1 + 1 } + + expect(value).to be(2) + end + end + end + end +end From 166ae89dfa36b8e844e58e61e3838200c4ab8ab7 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 15 Dec 2016 21:06:17 +0100 Subject: [PATCH 005/206] Ensure the session is destroyed --- lib/mattermost/session.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index d14121c91a0..f4629585da7 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -28,10 +28,12 @@ module Mattermost def with_session raise NoSessionError unless create - result = yield - destroy - result + begin + yield + ensure + destroy + end end # Next methods are needed for Doorkeeper From 178638c4b2fa6634dc0aaa0ef13fb5a14eb7021f Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 12 Dec 2016 09:31:48 +0100 Subject: [PATCH 006/206] Setup mattermost session --- lib/mattermost/mattermost.rb | 102 +++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ++++++++++ 2 files changed, 144 insertions(+) create mode 100644 lib/mattermost/mattermost.rb create mode 100644 spec/lib/mattermost/mattermost_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/mattermost.rb new file mode 100644 index 00000000000..84d016bb197 --- /dev/null +++ b/lib/mattermost/mattermost.rb @@ -0,0 +1,102 @@ +module Mattermost + class NoSessionError < StandardError; end + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Mattermost + include Doorkeeper::Helpers::Controller + include HTTParty + + attr_accessor :current_resource_owner + + def initialize(uri, current_user) + self.class.base_uri(uri) + + @current_resource_owner = current_user + end + + def with_session + raise NoSessionError unless create + yield + destroy + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(@oauth_uri.query).symbolize_keys + end + + private + + def create + return unless oauth_uri + return unless token_uri + + self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") + + request_token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri ||= URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= if @oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + @request_token ||= if @token_uri + response = get(@token_uri, follow_redirects: false) + response.headers['token'] if 200 <= response.code && response.code < 400 + end + end + + def get(path, options = {}) + self.class.get(path, options) + end + + def post(path, options = {}) + self.class.post(path, options) + end + end +end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb new file mode 100644 index 00000000000..7c99b4df9f3 --- /dev/null +++ b/spec/lib/mattermost/mattermost_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Mattermost::Mattermost do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorman to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + end + end + end +end From dd385c7c3d3046da18c6c251bce25afab1129662 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 15 Dec 2016 14:32:50 +0100 Subject: [PATCH 007/206] Improve session tests --- lib/mattermost/{mattermost.rb => session.rb} | 8 ++- spec/lib/mattermost/mattermost_spec.rb | 42 ------------ spec/lib/mattermost/session_spec.rb | 68 ++++++++++++++++++++ 3 files changed, 73 insertions(+), 45 deletions(-) rename lib/mattermost/{mattermost.rb => session.rb} (97%) delete mode 100644 spec/lib/mattermost/mattermost_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb diff --git a/lib/mattermost/mattermost.rb b/lib/mattermost/session.rb similarity index 97% rename from lib/mattermost/mattermost.rb rename to lib/mattermost/session.rb index 84d016bb197..81964666757 100644 --- a/lib/mattermost/mattermost.rb +++ b/lib/mattermost/session.rb @@ -13,7 +13,7 @@ module Mattermost # This class however skips the button click, and also the approval phase to # speed up the process and keep it without manual action and get a session # going. - class Mattermost + class Session include Doorkeeper::Helpers::Controller include HTTParty @@ -27,8 +27,10 @@ module Mattermost def with_session raise NoSessionError unless create - yield + result = yield destroy + + result end # Next methods are needed for Doorkeeper @@ -85,7 +87,7 @@ module Mattermost end def request_token - @request_token ||= if @token_uri + @request_token ||= begin response = get(@token_uri, follow_redirects: false) response.headers['token'] if 200 <= response.code && response.code < 400 end diff --git a/spec/lib/mattermost/mattermost_spec.rb b/spec/lib/mattermost/mattermost_spec.rb deleted file mode 100644 index 7c99b4df9f3..00000000000 --- a/spec/lib/mattermost/mattermost_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Mattermost do - let(:user) { create(:user) } - - subject { described_class.new('http://localhost:8065', user) } - - # Needed for doorman to function - it { is_expected.to respond_to(:current_resource_owner) } - it { is_expected.to respond_to(:request) } - it { is_expected.to respond_to(:authorization) } - it { is_expected.to respond_to(:strategy) } - - describe '#with session' do - let!(:stub) do - WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). - to_return(headers: { 'location' => 'http://mylocation.com' }, status: 307) - end - - context 'without oauth uri' do - it 'makes a request to the oauth uri' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - - context 'with oauth_uri' do - let!(:doorkeeper) do - Doorkeeper::Application.create(name: "GitLab Mattermost", - redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", - scopes: "") - end - - context 'without token_uri' do - it 'can not create a session' do - expect do - subject.with_session - end.to raise_error(Mattermost::NoSessionError) - end - end - end - end - end -end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..aa56a56db81 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Mattermost::Session do + let(:user) { create(:user) } + + subject { described_class.new('http://localhost:8065', user) } + + # Needed for doorman to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/oauth/gitlab/login'). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create(name: "GitLab Mattermost", + redirect_uri: "http://localhost:8065/signup/gitlab/complete\nhttp://localhost:8065/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "eyJhY3Rpb24iOiJsb2dpbiIsImhhc2giOiIkMmEkMTAkVC9wYVlEaTdIUS8vcWdKRmdOOUllZUptaUNJWUlvNVNtNEcwU2NBMXFqelNOVmVPZ1cxWUsifQ%3D%3D" } + let(:location) { "http://locahost:8065/oauth/authorize?response_type=code&client_id=#{doorkeeper.uid}&redirect_uri=http%3A%2F%2Flocalhost:8065%2Fsignup%2Fgitlab%2Fcomplete&state=#{state}" } + + before do + WebMock.stub_request(:get, /http:\/\/localhost:8065\/signup\/gitlab\/complete*/). + to_return(headers: { 'token' => 'thisworksnow' }, status: 202) + end + + it 'can setup a session' do + expect(subject).to receive(:destroy) + + subject.with_session { 1 + 1 } + end + + it 'returns the value of the block' do + WebMock.stub_request(:post, "http://localhost:8065/api/v3/users/logout"). + to_return(headers: { 'token' => 'thisworksnow' }, status: 200) + + value = subject.with_session { 1 + 1 } + + expect(value).to be(2) + end + end + end + end +end From 87d160634dfdaacd0dc382c26932786382d1be34 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 13 Dec 2016 19:52:41 +0100 Subject: [PATCH 008/206] Base work for auto config MM slash commands --- .../projects/services_controller.rb | 28 ++- app/helpers/mattermost_helper.rb | 13 ++ .../mattermost_slash_commands_service.rb | 15 ++ app/models/service.rb | 4 + app/views/projects/services/_form.html.haml | 20 +- .../mattermost_slash_commands/_form.html.haml | 3 + .../mattermost_slash_commands/_help.html.haml | 195 +++++++++--------- app/views/shared/_service_settings.html.haml | 59 +++--- config/gitlab.yml.example | 5 + config/routes/project.rb | 1 + lib/mattermost/command.rb | 26 +++ lib/mattermost/session.rb | 13 +- lib/mattermost/team.rb | 10 + 13 files changed, 252 insertions(+), 140 deletions(-) create mode 100644 app/helpers/mattermost_helper.rb create mode 100644 app/views/projects/services/mattermost_slash_commands/_form.html.haml create mode 100644 lib/mattermost/command.rb create mode 100644 lib/mattermost/team.rb diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 30c2a5d9982..94ea36bbdd9 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -3,7 +3,7 @@ class Projects::ServicesController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :service, only: [:edit, :update, :test] + before_action :service, only: [:edit, :update, :test, :configure] respond_to :html @@ -44,9 +44,35 @@ class Projects::ServicesController < Projects::ApplicationController redirect_back_or_default(options: message) end + def configure + host = Gitlab.config.mattermost.host + if @service.auto_config? && host + @service.configure(host, current_user, params) + + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + notice: 'This service is now configured.' + ) + else + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + alert: 'This service can not be automatticly configured.' + ) + end + rescue Mattermost::NoSessionError + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + alert: 'An error occurred, is Mattermost configured with Single Sign on?' + ) + end + private def service @service ||= @project.find_or_initialize_service(params[:id]) end + + def configure_params + params.require(:auto_configure).permit(:trigger, :team_id) + end end diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb new file mode 100644 index 00000000000..83434c20c2b --- /dev/null +++ b/app/helpers/mattermost_helper.rb @@ -0,0 +1,13 @@ +module MattermostHelper + def mattermost_teams_for(current_user) + return unless Gitlab.config.mattermost.enabled + # Hack to make frontend work better + return [{"id"=>"qz8gdr1fopncueb8n9on8ohk3h", "create_at"=>1479992105904, "update_at"=>1479992105904, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"admin@example.com", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"gthxi47gj7rxtcx6zama63zd1w", "allow_open_invite"=>false}] + + + host = Gitlab.config.mattermost.host + Mattermost::Mattermost.new(host, current_user).with_session do + Mattermost::Team.all + end + end +end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 33431f41dc2..c0e8e1a9324 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,6 +25,21 @@ class MattermostSlashCommandsService < ChatService ] end + def auto_config? + Gitlab.config.mattermost.enabled + end + + def configure(host, current_user, params) + token = Mattermost::Mattermost.new(host, current_user).with_session do + Mattermost::Commands.create(params[:team_id], + trigger: params[:trigger] || @service.project.path, + url: service_trigger_url(@service), + icon_url: asset_url('gitlab_logo.png')) + end + + update_attributes(token: token) + end + def trigger(params) return nil unless valid_token?(params[:token]) diff --git a/app/models/service.rb b/app/models/service.rb index e49a8fa2904..9004d9caa19 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -54,6 +54,10 @@ class Service < ActiveRecord::Base template end + def auto_config? + false + end + def category read_attribute(:category).to_sym end diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index db51c4f8a4e..0160a366aaa 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -6,16 +6,12 @@ %p= @service.description .col-lg-9 - = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| - = render 'shared/service_settings', form: form, subject: @service + - # This returns an array of hashes, could you make a fancy dropdown :D + - # Also, this is mocked for now, checkout the MattermostHelper to edit the data + = mattermost_teams_for(current_user) + = form_for(:auto_configure, method: :post, url: configure_namespace_project_service_path(@project.namespace, @project, @service.to_param)) do |f| + = "Team ID" + = f.text_field(:team_id) + = "Team ID" + = f.submit 'Save changes', class: 'btn btn-save' - .footer-block.row-content-block - = form.submit 'Save changes', class: 'btn btn-save' -   - - if @service.valid? && @service.activated? - - unless @service.can_test? - - disabled_class = 'disabled' - - disabled_title = @service.disabled_title - - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/mattermost_slash_commands/_form.html.haml b/app/views/projects/services/mattermost_slash_commands/_form.html.haml new file mode 100644 index 00000000000..9d9c877e791 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_form.html.haml @@ -0,0 +1,3 @@ +- teams = Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do + Mattermost::Mattermost::Team.all + end diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index a676c0290a0..70f2ef52135 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,100 +1,101 @@ - pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" - run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" -.well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Mattermost. - %br - See list of available commands in Mattermost after setting up this service, - by entering - %code /<command_trigger_word> help - %br - %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' - on your Mattermost installation - %li - 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - - %hr - - .help-form - .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#display_name') - - .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#description') - - .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - - .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#request_url') - - .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST - - .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_username') - - .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') - - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes - - .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') - - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') - - %hr - - %ul.list-unstyled - %li - 3. After adding the slash command, paste the - %strong token - into the field below +- unless GitLab.config.mattermost.enabled + .well + This service allows GitLab users to perform common operations on this + project by entering slash commands in Mattermost. + %br + See list of available commands in Mattermost after setting up this service, + by entering + %code /<command_trigger_word> help + %br + %br + To setup this service: + %ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + + %hr + + .help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + + %hr + + %ul.list-unstyled + %li + 3. After adding the slash command, paste the + %strong token + into the field below diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 9c5053dace5..e8ab1b2ed46 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -13,38 +13,41 @@ .col-sm-10 = form.check_box :active - - if @service.supported_events.present? - .form-group - = form.label :url, "Trigger", class: 'control-label' + - if @service.auto_config? - .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize + - else + - if @service.supported_events.present? + .form-group + = form.label :url, "Trigger", class: 'control-label' - - field = @service.event_field(event) + .col-sm-10 + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + - field = @service.event_field(event) - %p.light - = service_event_description(event) + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - @service.global_fields.each do |field| - - type = field[:type] + %p.light + = service_event_description(event) - - if type == 'fieldset' - - fields = field[:fields] - - legend = field[:legend] + - @service.global_fields.each do |field| + - type = field[:type] - %fieldset - %legend= legend - - fields.each do |subfield| - = render 'shared/field', form: form, field: subfield - - else - = render 'shared/field', form: form, field: field + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] + + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 327e4a7937c..e1e76e8bf73 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,6 +153,11 @@ production: &base # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects + # For executing commands from GitLab on Mattermost + mattermost: + enabled: false + host: 'http://locahost:8065' + ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: diff --git a/config/routes/project.rb b/config/routes/project.rb index 0754f0ec3b0..6f480b9e1a0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -61,6 +61,7 @@ constraints(ProjectUrlConstrainer.new) do resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do member do get :test + post :configure end end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb new file mode 100644 index 00000000000..e159458a788 --- /dev/null +++ b/lib/mattermost/command.rb @@ -0,0 +1,26 @@ +module Mattermost + class Command + def self.all(team_id) + Mattermost::Mattermost.get("/teams/#{team_id}/commands/list_team_commands") + end + + # params should be a hash, which supplies _at least_: + # - trigger => The slash command, no spaces, cannot start with a / + # - url => What is the URL to trigger here? + # - icon_url => Supply a link to the icon + def self.create(team_id, params) + params = { + auto_complete: true, + auto_complete_desc: 'List all available commands', + auto_complete_hint: '[help]', + description: 'Perform common operations on GitLab', + display_name: 'GitLab', + method: 'P', + user_name: 'GitLab' + }..merge(params) + + Mattermost::Mattermost.post( "/teams/#{team_id}/commands/create", params.to_json). + parsed_response['token'] + end + end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 81964666757..cc4cb1f4f12 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -20,6 +20,7 @@ module Mattermost attr_accessor :current_resource_owner def initialize(uri, current_user) + uri = normalize_uri(uri) self.class.base_uri(uri) @current_resource_owner = current_user @@ -31,6 +32,8 @@ module Mattermost destroy result + rescue Errno::ECONNREFUSED + raise NoSessionError end # Next methods are needed for Doorkeeper @@ -67,11 +70,11 @@ module Mattermost end def destroy - post('/api/v3/users/logout') + post('/users/logout') end def oauth_uri - response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + response = get("/oauth/gitlab/login", follow_redirects: false) return unless 300 <= response.code && response.code < 400 redirect_uri = response.headers['location'] @@ -100,5 +103,11 @@ module Mattermost def post(path, options = {}) self.class.post(path, options) end + + def normalize_uri(uri) + uri << '/' unless uri.end_with?('/') + + uri << 'api/v3' + end end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb new file mode 100644 index 00000000000..76e238a866e --- /dev/null +++ b/lib/mattermost/team.rb @@ -0,0 +1,10 @@ +module Mattermost + class Team < Mattermost + # After normalization this returns an array of hashes + # + # [{"id"=>"paf573pj9t81urupw3fanozeda", "display_name"=>"my team", }] + def self.all + @all_teams ||= get('/teams/all').parsed_response.values + end + end +end From 99d8d6f0d48e28f5ba798d1d4461071a01435054 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 13 Dec 2016 19:52:41 +0100 Subject: [PATCH 009/206] Add MattermostController --- .../projects/mattermost_controller.rb | 47 +++++ app/views/projects/mattermost/new.html.haml | 6 + .../mattermost_slash_commands/_help.html.haml | 198 +++++++++--------- config/routes/project.rb | 6 + 4 files changed, 159 insertions(+), 98 deletions(-) create mode 100644 app/controllers/projects/mattermost_controller.rb create mode 100644 app/views/projects/mattermost/new.html.haml diff --git a/app/controllers/projects/mattermost_controller.rb b/app/controllers/projects/mattermost_controller.rb new file mode 100644 index 00000000000..f50f921c7cf --- /dev/null +++ b/app/controllers/projects/mattermost_controller.rb @@ -0,0 +1,47 @@ +class Projects::MattermostController < Projects::ApplicationController + layout 'project_settings' + before_action :authorize_admin_project! + before_action :service + before_action :teams, only: [:new] + + def new + end + + def configure + @service.configure(host, current_user, params) + + redirect_to( + new_namespace_project_service_path(@project.namespace, @project, @service.to_param), + notice: 'This service is now configured.' + ) + rescue Mattermost::NoSessionError + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + alert: 'No session could be set up, is Mattermost configured with Single Sign on?' + ) + end + + private + + def configure_params + params.require(:configure_params).permit(:trigger, :team_id) + end + + def service + @service ||= @project.services.find_by(type: 'MattermostSlashCommandsService') + end + + def teams + # Mocking for frontend development + @teams = [{"id"=>"qz8gdr1fopncueb8n9on8ohk3h", "create_at"=>1479992105904, "update_at"=>1479992105904, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"admin@example.com", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"gthxi47gj7rxtcx6zama63zd1w", "allow_open_invite"=>false}] + + # @teams = + # begin + # Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do + # Mattermost::Team.all + # end + # rescue Mattermost::NoSessionError + # @teams = [] + # end + end +end diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml new file mode 100644 index 00000000000..a7e028bc0cb --- /dev/null +++ b/app/views/projects/mattermost/new.html.haml @@ -0,0 +1,6 @@ += "hello world" += form_for(:create, method: :post, url: configure_namespace_project_mattermost_path(@project.namespace, @project, @service.to_param)) do |f| + = "Team ID" + = f.text_field(:team_id) + = f.submit 'Configure', class: 'btn btn-save' + diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 70f2ef52135..6494d3cd793 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,101 +1,103 @@ - pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" - run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" -- unless GitLab.config.mattermost.enabled - .well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Mattermost. - %br - See list of available commands in Mattermost after setting up this service, - by entering - %code /<command_trigger_word> help - %br - %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' - on your Mattermost installation - %li - 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - - %hr - - .help-form - .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#display_name') - - .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#description') - - .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - - .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#request_url') - - .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST - - .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_username') - - .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') - - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes - - .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') - - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') - - %hr - - %ul.list-unstyled - %li - 3. After adding the slash command, paste the - %strong token - into the field below +.well + This service allows GitLab users to perform common operations on this + project by entering slash commands in Mattermost. + %br + See list of available commands in Mattermost after setting up this service, + by entering + %code /<command_trigger_word> help + %br + %br + To setup this service: + %ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + + %hr + + .help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + + %hr + + %ul.list-unstyled + %li + 3. After adding the slash command, paste the + %strong token + into the field below + +- if Gitlab.config.mattermost.enabled + = link_to "Auto config", new_namespace_project_mattermost_path(@project.namespace, @project) diff --git a/config/routes/project.rb b/config/routes/project.rb index 6f480b9e1a0..4ce09b603a2 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -65,6 +65,12 @@ constraints(ProjectUrlConstrainer.new) do end end + resources :mattermost, only: [:new] do + member do + post :configure + end + end + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do put :enable From 0045996728308bcb7643618ab48efb7e04e7d4bf Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 15 Dec 2016 20:19:42 +0100 Subject: [PATCH 010/206] Add auto configure of commands --- .../projects/mattermost_controller.rb | 19 ++++++++----------- .../mattermost_slash_commands_service.rb | 4 ---- app/models/service.rb | 4 ---- lib/mattermost/command.rb | 16 +++++++++------- lib/mattermost/session.rb | 2 +- lib/mattermost/team.rb | 19 +++++++++++++------ spec/lib/mattermost/team_spec.rb | 19 +++++++++++++++++++ 7 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/app/controllers/projects/mattermost_controller.rb b/app/controllers/projects/mattermost_controller.rb index f50f921c7cf..f04189c8775 100644 --- a/app/controllers/projects/mattermost_controller.rb +++ b/app/controllers/projects/mattermost_controller.rb @@ -32,16 +32,13 @@ class Projects::MattermostController < Projects::ApplicationController end def teams - # Mocking for frontend development - @teams = [{"id"=>"qz8gdr1fopncueb8n9on8ohk3h", "create_at"=>1479992105904, "update_at"=>1479992105904, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"admin@example.com", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"gthxi47gj7rxtcx6zama63zd1w", "allow_open_invite"=>false}] - - # @teams = - # begin - # Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do - # Mattermost::Team.all - # end - # rescue Mattermost::NoSessionError - # @teams = [] - # end + @teams = + begin + Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do + Mattermost::Team.team_admin + end + rescue Mattermost::NoSessionError + [] + end end end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index c0e8e1a9324..e07cc0e4077 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,10 +25,6 @@ class MattermostSlashCommandsService < ChatService ] end - def auto_config? - Gitlab.config.mattermost.enabled - end - def configure(host, current_user, params) token = Mattermost::Mattermost.new(host, current_user).with_session do Mattermost::Commands.create(params[:team_id], diff --git a/app/models/service.rb b/app/models/service.rb index 9004d9caa19..e49a8fa2904 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -54,10 +54,6 @@ class Service < ActiveRecord::Base template end - def auto_config? - false - end - def category read_attribute(:category).to_sym end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index e159458a788..b6446935eb6 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,7 +1,7 @@ module Mattermost - class Command + class Command < Session def self.all(team_id) - Mattermost::Mattermost.get("/teams/#{team_id}/commands/list_team_commands") + get("/teams/#{team_id}/commands/list_team_commands").parsed_response end # params should be a hash, which supplies _at least_: @@ -9,18 +9,20 @@ module Mattermost # - url => What is the URL to trigger here? # - icon_url => Supply a link to the icon def self.create(team_id, params) - params = { + command = { auto_complete: true, auto_complete_desc: 'List all available commands', auto_complete_hint: '[help]', description: 'Perform common operations on GitLab', display_name: 'GitLab', method: 'P', - user_name: 'GitLab' - }..merge(params) + user_name: 'GitLab', + trigger: 'gitlab', + }.merge(params) - Mattermost::Mattermost.post( "/teams/#{team_id}/commands/create", params.to_json). - parsed_response['token'] + response = post( "/teams/#{team_id}/commands/create", body: command.to_json) + + response.parsed_response['token'] end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index cc4cb1f4f12..15bf95a38c9 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -65,6 +65,7 @@ module Mattermost return unless token_uri self.class.headers("Cookie" => "MMAUTHTOKEN=#{request_token}") + self.class.headers("X-Requested-With" => 'XMLHttpRequest') request_token end @@ -106,7 +107,6 @@ module Mattermost def normalize_uri(uri) uri << '/' unless uri.end_with?('/') - uri << 'api/v3' end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 76e238a866e..54d029cb022 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,10 +1,17 @@ module Mattermost - class Team < Mattermost - # After normalization this returns an array of hashes - # - # [{"id"=>"paf573pj9t81urupw3fanozeda", "display_name"=>"my team", }] - def self.all - @all_teams ||= get('/teams/all').parsed_response.values + class Team < Session + def self.team_admin + body = get('/users/initial_load').parsed_response + + return [] unless body['team_members'] + + team_ids = body['team_members'].map do |team| + team['team_id'] if team['roles'].split.include?('team_admin') + end.compact + + body['teams'].select do |team| + team_ids.include?(team['id']) + end end end end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..a3b0831659f --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Mattermost::Team do + let(:session) { Mattermost::Session.new('http://localhost:8065/', nil) } + + describe '.all' do + let(:result) { {id: 'abc', display_name: 'team'} } + before do + WebMock.stub_request(:get, 'http://localhost:8065/api/v3/teams/all'). + and_return({ abc: result }.to_json) + end + + xit 'gets the teams' do + allow(session).to receive(:with_session) { yield } + + expect(described_class.all).to eq(result) + end + end +end From 9046b583ff61d74c8e481053d150da924d129ad8 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 16 Dec 2016 12:51:12 +0100 Subject: [PATCH 011/206] fixed variable, plus added light text color for light badges --- app/assets/stylesheets/framework/badges.scss | 1 + app/assets/stylesheets/framework/variables.scss | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index 89e1c7890d8..e9d7cda0647 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -7,4 +7,5 @@ .badge-dark { background-color: $badge-bg-dark; + color: $badge-color-dark; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index cdc54fdc254..288bb67c4bf 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -281,7 +281,8 @@ $btn-active-gray-light: e4e7ed; */ $badge-bg: #f3f3f3; $badge-bg-dark: #eee; -$badge-color: $btn-transparent-color; +$badge-color: #929292; +$badge-color-dark: #8f8f8f; /* * Award emoji From 7363a7d3b5804493f86531bebb1610afb91b5293 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 16 Dec 2016 15:45:56 +0100 Subject: [PATCH 012/206] Add tests for auto configure slash commands --- .../projects/services_controller.rb | 26 -------- app/helpers/mattermost_helper.rb | 13 ---- app/views/projects/services/_form.html.haml | 20 ++++--- .../mattermost_slash_commands/_form.html.haml | 3 - app/views/shared/_service_settings.html.haml | 59 +++++++++---------- config/routes/project.rb | 1 - lib/mattermost/command.rb | 8 ++- lib/mattermost/team.rb | 14 +++-- spec/fixtures/mattermost_initial_load.json | 1 + spec/fixtures/mattermost_new_command.json | 1 + spec/lib/mattermost/command_spec.rb | 17 ++++++ spec/lib/mattermost/team_spec.rb | 23 ++++---- .../mattermost_slash_commands_service_spec.rb | 31 ++++++++++ 13 files changed, 118 insertions(+), 99 deletions(-) delete mode 100644 app/helpers/mattermost_helper.rb delete mode 100644 app/views/projects/services/mattermost_slash_commands/_form.html.haml create mode 100644 spec/fixtures/mattermost_initial_load.json create mode 100644 spec/fixtures/mattermost_new_command.json create mode 100644 spec/lib/mattermost/command_spec.rb diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 94ea36bbdd9..b4f5750b0a4 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -44,35 +44,9 @@ class Projects::ServicesController < Projects::ApplicationController redirect_back_or_default(options: message) end - def configure - host = Gitlab.config.mattermost.host - if @service.auto_config? && host - @service.configure(host, current_user, params) - - redirect_to( - edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), - notice: 'This service is now configured.' - ) - else - redirect_to( - edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), - alert: 'This service can not be automatticly configured.' - ) - end - rescue Mattermost::NoSessionError - redirect_to( - edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), - alert: 'An error occurred, is Mattermost configured with Single Sign on?' - ) - end - private def service @service ||= @project.find_or_initialize_service(params[:id]) end - - def configure_params - params.require(:auto_configure).permit(:trigger, :team_id) - end end diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb deleted file mode 100644 index 83434c20c2b..00000000000 --- a/app/helpers/mattermost_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module MattermostHelper - def mattermost_teams_for(current_user) - return unless Gitlab.config.mattermost.enabled - # Hack to make frontend work better - return [{"id"=>"qz8gdr1fopncueb8n9on8ohk3h", "create_at"=>1479992105904, "update_at"=>1479992105904, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"admin@example.com", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"gthxi47gj7rxtcx6zama63zd1w", "allow_open_invite"=>false}] - - - host = Gitlab.config.mattermost.host - Mattermost::Mattermost.new(host, current_user).with_session do - Mattermost::Team.all - end - end -end diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 0160a366aaa..db51c4f8a4e 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -6,12 +6,16 @@ %p= @service.description .col-lg-9 - - # This returns an array of hashes, could you make a fancy dropdown :D - - # Also, this is mocked for now, checkout the MattermostHelper to edit the data - = mattermost_teams_for(current_user) - = form_for(:auto_configure, method: :post, url: configure_namespace_project_service_path(@project.namespace, @project, @service.to_param)) do |f| - = "Team ID" - = f.text_field(:team_id) - = "Team ID" - = f.submit 'Save changes', class: 'btn btn-save' + = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| + = render 'shared/service_settings', form: form, subject: @service + .footer-block.row-content-block + = form.submit 'Save changes', class: 'btn btn-save' +   + - if @service.valid? && @service.activated? + - unless @service.can_test? + - disabled_class = 'disabled' + - disabled_title = @service.disabled_title + + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/mattermost_slash_commands/_form.html.haml b/app/views/projects/services/mattermost_slash_commands/_form.html.haml deleted file mode 100644 index 9d9c877e791..00000000000 --- a/app/views/projects/services/mattermost_slash_commands/_form.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- teams = Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do - Mattermost::Mattermost::Team.all - end diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index e8ab1b2ed46..9c5053dace5 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -13,41 +13,38 @@ .col-sm-10 = form.check_box :active - - if @service.auto_config? + - if @service.supported_events.present? + .form-group + = form.label :url, "Trigger", class: 'control-label' - - else - - if @service.supported_events.present? - .form-group - = form.label :url, "Trigger", class: 'control-label' + .col-sm-10 + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize - .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize + - field = @service.event_field(event) - - field = @service.event_field(event) + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + %p.light + = service_event_description(event) - %p.light - = service_event_description(event) + - @service.global_fields.each do |field| + - type = field[:type] - - @service.global_fields.each do |field| - - type = field[:type] + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] - - if type == 'fieldset' - - fields = field[:fields] - - legend = field[:legend] - - %fieldset - %legend= legend - - fields.each do |subfield| - = render 'shared/field', form: form, field: subfield - - else - = render 'shared/field', form: form, field: field + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/config/routes/project.rb b/config/routes/project.rb index 4ce09b603a2..3e210a75df5 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -61,7 +61,6 @@ constraints(ProjectUrlConstrainer.new) do resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do member do get :test - post :configure end end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 108a2a47a4b..7d4710bb94d 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -14,9 +14,13 @@ module Mattermost icon_url: icon_url } - response = post( "/teams/#{team_id}/commands/create", body: command.to_json) + post_command(command)['token'] + end - response.parsed_response['token'] + private + + def post_command(command) + post( "/teams/#{team_id}/commands/create", body: command.to_json).parsed_response end end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 54d029cb022..714748aea3c 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,17 +1,21 @@ module Mattermost class Team < Session def self.team_admin - body = get('/users/initial_load').parsed_response + return [] unless initial_load['team_members'] - return [] unless body['team_members'] - - team_ids = body['team_members'].map do |team| + team_ids = initial_load['team_members'].map do |team| team['team_id'] if team['roles'].split.include?('team_admin') end.compact - body['teams'].select do |team| + initial_load['teams'].select do |team| team_ids.include?(team['id']) end end + + private + + def initial_load + @initial_load ||= get('/users/initial_load').parsed_response + end end end diff --git a/spec/fixtures/mattermost_initial_load.json b/spec/fixtures/mattermost_initial_load.json new file mode 100644 index 00000000000..89e35f8ff15 --- /dev/null +++ b/spec/fixtures/mattermost_initial_load.json @@ -0,0 +1 @@ +{"user":{"id":"78nm4euoc7dypergdc13ekxgpo","create_at":1481826051672,"update_at":1481835484207,"delete_at":0,"username":"root","auth_data":"","auth_service":"gitlab","email":"admin@example.com","email_verified":true,"nickname":"","first_name":"Administrator","last_name":"","roles":"system_admin system_user","notify_props":{"channel":"true","desktop":"all","desktop_sound":"true","email":"true","first_name":"true","mention_keys":"root,@root","push":"mention"},"last_password_update":1481826051672,"locale":"en"},"team_members":[{"team_id":"w59qt5a817f69jkxdz6xe7y4ir","user_id":"78nm4euoc7dypergdc13ekxgpo","roles":"team_user team_admin","delete_at":0},{"team_id":"my9oujxf5jy1zqdgu9rihd66do","user_id":"78nm4euoc7dypergdc13ekxgpo","roles":"team_user team_admin","delete_at":0}],"teams":[{"id":"w59qt5a817f69jkxdz6xe7y4ir","create_at":1481835484179,"update_at":1481835484179,"delete_at":0,"display_name":"new_team","name":"new-team","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"mfgsqnmpiby18eepo6jd6pq3oh","allow_open_invite":false},{"id":"my9oujxf5jy1zqdgu9rihd66do","create_at":1481826062406,"update_at":1481826062406,"delete_at":0,"display_name":"chatops","name":"chatops","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"s7c1phenmi8udkybcyytc3pxuh","allow_open_invite":false}],"preferences":[{"user_id":"78nm4euoc7dypergdc13ekxgpo","category":"last","name":"channel","value":"u4j58zgjyt8zd8nwwhaqjkyqzw"},{"user_id":"78nm4euoc7dypergdc13ekxgpo","category":"tutorial_step","name":"78nm4euoc7dypergdc13ekxgpo","value":"999"}],"client_cfg":{"AboutLink":"/static/help/about.html","AndroidAppDownloadLink":"https://about.mattermost.com/mattermost-android-app/","AppDownloadLink":"https://about.mattermost.com/downloads/","AvailableLocales":"","BuildDate":"Wed Nov 23 19:43:58 UTC 2016","BuildEnterpriseReady":"false","BuildHash":"36f62c9e82350f58c902f64a5d3304872431ad41","BuildHashEnterprise":"none","BuildNumber":"3.5.1","DefaultClientLocale":"en","EnableCommands":"true","EnableCustomEmoji":"false","EnableDeveloper":"false","EnableDiagnostics":"true","EnableEmailBatching":"false","EnableIncomingWebhooks":"false","EnableOAuthServiceProvider":"false","EnableOnlyAdminIntegrations":"true","EnableOpenServer":"false","EnableOutgoingWebhooks":"false","EnablePostIconOverride":"true","EnablePostUsernameOverride":"true","EnablePublicLink":"true","EnableSignInWithEmail":"true","EnableSignInWithUsername":"false","EnableSignUpWithEmail":"false","EnableSignUpWithGitLab":"true","EnableTeamCreation":"true","EnableTesting":"false","EnableUserCreation":"true","EnableWebrtc":"false","GoogleDeveloperKey":"","HelpLink":"","IosAppDownloadLink":"https://about.mattermost.com/mattermost-ios-app/","MaxFileSize":"52428800","PrivacyPolicyLink":"/static/help/privacy.html","ProfileHeight":"128","ProfileWidth":"128","ReportAProblemLink":"/static/help/report_problem.html","RequireEmailVerification":"false","RestrictCustomEmojiCreation":"all","RestrictDirectMessage":"any","RestrictPrivateChannelManagement":"all","RestrictPublicChannelManagement":"all","RestrictTeamInvite":"all","SQLDriverName":"postgres","SegmentDeveloperKey":"","SendEmailNotifications":"false","SendPushNotifications":"false","ShowEmailAddress":"true","SiteName":"GitLab Mattermost","SiteURL":"","SupportEmail":"support@example.com","TermsOfServiceLink":"/static/help/terms.html","Version":"3.5.0","WebsocketPort":"80","WebsocketSecurePort":"443"},"license_cfg":{"IsLicensed":"false"},"no_accounts":false} diff --git a/spec/fixtures/mattermost_new_command.json b/spec/fixtures/mattermost_new_command.json new file mode 100644 index 00000000000..4b827f19926 --- /dev/null +++ b/spec/fixtures/mattermost_new_command.json @@ -0,0 +1 @@ +{"id":"y8j1nexrdirj5nubq5uzdwwidr","token":"pzajm5hfbtni3r49ujpt8betpc","create_at":1481897117122,"update_at":1481897117122,"delete_at":0,"creator_id":"78nm4euoc7dypergdc13ekxgpo","team_id":"w59qt5a817f69jkxdz6xe7y4ir","trigger":"display","method":"P","username":"GitLab","icon_url":"","auto_complete":false,"auto_complete_desc":"","auto_complete_hint":"","display_name":"Display name","description":"the description","url":"http://trigger.url/trigger"} diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..7c6457f639d --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Mattermost::Command do + describe '.create' do + let(:new_command) do + JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_new_command.json'))) + end + + it 'gets the teams' do + allow(described_class).to receive(:post_command).and_return(new_command) + + token = described_class.create('abc', url: 'http://trigger.url/trigger', icon_url: 'http://myicon.com/icon.png') + + expect(token).to eq('pzajm5hfbtni3r49ujpt8betpc') + end + end +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index a3b0831659f..0fe6163900d 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -1,19 +1,22 @@ require 'spec_helper' describe Mattermost::Team do - let(:session) { Mattermost::Session.new('http://localhost:8065/', nil) } - - describe '.all' do - let(:result) { {id: 'abc', display_name: 'team'} } - before do - WebMock.stub_request(:get, 'http://localhost:8065/api/v3/teams/all'). - and_return({ abc: result }.to_json) + describe '.team_admin' do + let(:init_load) do + JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json'))) end - xit 'gets the teams' do - allow(session).to receive(:with_session) { yield } + before do + allow(described_class).to receive(:initial_load).and_return(init_load) + end - expect(described_class.all).to eq(result) + it 'gets the teams' do + expect(described_class.team_admin.count).to be(2) + end + + it 'filters on being team admin' do + ids = described_class.team_admin.map { |team| team['id'] } + expect(ids).to include("w59qt5a817f69jkxdz6xe7y4ir", "my9oujxf5jy1zqdgu9rihd66do") end end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 4a1037e950b..43b2c2c1302 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -96,4 +96,35 @@ describe MattermostSlashCommandsService, models: true do end end end + + describe '#configure' do + let(:project) { create(:empty_project) } + let(:service) { project.build_mattermost_slash_commands_service } + + subject do + service.configure('http://localhost:8065', nil, team_id: 'abc', trigger: 'gitlab', url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png') + end + + it 'creates a new Mattermost session' do + expect_any_instance_of(Mattermost::Session).to receive(:with_session) + + subject + end + + it 'saves the service' do + allow_any_instance_of(Mattermost::Session).to receive(:with_session). + and_return('mynewtoken') + + expect { subject }.to change { project.services.count }.by(1) + end + + it 'saves the token' do + allow_any_instance_of(Mattermost::Session).to receive(:with_session). + and_return('mynewtoken') + + subject + + expect(service.reload.token).to eq('mynewtoken') + end + end end From f0889bdfa3e0d1433b3cd293859b13ee9d186ab6 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 16 Dec 2016 20:29:17 +0100 Subject: [PATCH 013/206] Incorporate review --- .../projects/services_controller.rb | 2 +- .../mattermost_slash_commands_service.rb | 26 ++++++++++++++----- app/views/projects/mattermost/new.html.haml | 2 -- lib/mattermost/command.rb | 12 --------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index b4f5750b0a4..30c2a5d9982 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -3,7 +3,7 @@ class Projects::ServicesController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :service, only: [:edit, :update, :test, :configure] + before_action :service, only: [:edit, :update, :test] respond_to :html diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 5af85d9a598..5dfc4cc2744 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,15 +25,12 @@ class MattermostSlashCommandsService < ChatService ] end - def configure(host, current_user, team_id:, trigger:, url:, icon_url:) + def configure(host, current_user, params) new_token = Mattermost::Session.new(host, current_user).with_session do - Mattermost::Command.create(team_id, - trigger: trigger || @service.project.path, - url: url, - icon_url: icon_url) + Mattermost::Command.create(params[:team_id], command) end - update!(token: new_token) + update!(token: new_token, active: true) end def trigger(params) @@ -50,6 +47,23 @@ class MattermostSlashCommandsService < ChatService private + def command(trigger:, url:, icon_url:) + pretty_project_name = project.name_with_namespace + + { + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{pretty_project_name}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{pretty_project_name}", + display_name: "GitLab / #{pretty_project_name}", + method: 'P', + user_name: 'GitLab', + trigger: trigger, + url: url, + icon_url: icon_url + } + end + def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index b4a1476be13..88270985e7b 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -1,5 +1,3 @@ -= "hello world" -= @teams = form_for(:create, method: :post, url: configure_namespace_project_mattermost_path(@project.namespace, @project, )) do |f| = "Team ID" = f.text_field(:team_id) diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 7d4710bb94d..9c37d0b0d79 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,18 +1,6 @@ module Mattermost class Command < Session def self.create(team_id, trigger: 'gitlab', url:, icon_url:) - command = { - auto_complete: true, - auto_complete_desc: 'List all available commands', - auto_complete_hint: '[help]', - description: 'Perform common operations on GitLab', - display_name: 'GitLab Slash Commands', - method: 'P', - user_name: 'GitLab', - trigger: trigger, - url: url, - icon_url: icon_url - } post_command(command)['token'] end From 24dd95756e267413a6f4e238eef06f8378dcaf63 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 17 Dec 2016 12:29:12 +0000 Subject: [PATCH 014/206] Change code font size to 12px --- app/assets/stylesheets/framework/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d0c27d64239..d85d3f968d3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -133,7 +133,7 @@ $md-area-border: #ddd; /* * Code */ -$code_font_size: 13px; +$code_font_size: 12px; $code_line_height: 1.5; /* From b23f32a735459f1c76ac8232f9de06942b1d2f8a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 17 Dec 2016 18:28:36 +0000 Subject: [PATCH 015/206] Fix configure route --- app/views/projects/mattermost/new.html.haml | 3 +-- config/routes/project.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index 88270985e7b..1def3b07300 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -1,5 +1,4 @@ -= form_for(:create, method: :post, url: configure_namespace_project_mattermost_path(@project.namespace, @project, )) do |f| += form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| = "Team ID" = f.text_field(:team_id) = f.submit 'Configure', class: 'btn btn-save' - diff --git a/config/routes/project.rb b/config/routes/project.rb index 3e210a75df5..3eddd3c2a0e 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -65,7 +65,7 @@ constraints(ProjectUrlConstrainer.new) do end resources :mattermost, only: [:new] do - member do + collection do post :configure end end From a701b1fcbbdc46918a53126bd59fa1c550e681dc Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 16 Dec 2016 18:13:52 +0000 Subject: [PATCH 016/206] Started on new slash commands edit view --- app/assets/stylesheets/pages/projects.scss | 20 ++++ app/helpers/projects_helper.rb | 11 ++- app/views/projects/mattermost/new.html.haml | 60 +++++++++++- app/views/projects/services/_form.html.haml | 20 ++-- .../mattermost_slash_commands/_help.html.haml | 96 +------------------ .../_installation_info.html.haml | 31 ++++++ app/views/shared/_service_settings.html.haml | 67 ++++++------- .../shared/icons/_mattermost_logo.svg.erb | 1 + 8 files changed, 163 insertions(+), 143 deletions(-) create mode 100644 app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml create mode 100644 app/views/shared/icons/_mattermost_logo.svg.erb diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9c3dbc58ae0..91173915a54 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -888,3 +888,23 @@ pre.light-well { width: 30%; } } + +.services-installation-info .row { + margin-bottom: 10px; +} + +.service-installation { + padding: 32px; + margin: 32px; + border-radius: 3px; + background-color: $white-light; + + h3 { + margin-top: 0; + } + + hr { + margin: 32px 0; + border-color: $border-color; + } +} diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9cda3b78761..1915bd833fe 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -148,6 +148,15 @@ module ProjectsHelper ).html_safe end + def mattermost_teams_options(teams) + teams_options = teams.map do |team| + return nil unless team['display_name'] && team['id'] + [team['display_name'], team['id']] + end.compact + teams_options.unshift(['Select a team...', '0']) unless teams_options.count === 1 + teams_options + end + private def repo_children_classes(field) @@ -390,7 +399,7 @@ module ProjectsHelper "success" end end - + def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index 1def3b07300..6a5c9df543d 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -1,4 +1,56 @@ -= form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| - = "Team ID" - = f.text_field(:team_id) - = f.submit 'Configure', class: 'btn btn-save' +- twoTeams = [{"id"=>"w59qt5a817f69jkxdz6xe7y4ir", "create_at"=>1481835484179, "update_at"=>1481835484179, "delete_at"=>0, "display_name"=>"new_team", "name"=>"new-team", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"mfgsqnmpiby18eepo6jd6pq3oh", "allow_open_invite"=>false}, {"id"=>"my9oujxf5jy1zqdgu9rihd66do", "create_at"=>1481826062406, "update_at"=>1481826062406, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"s7c1phenmi8udkybcyytc3pxuh", "allow_open_invite"=>false}] +- oneTeams = [{"id"=>"w59qt5a817f69jkxdz6xe7y4ir", "create_at"=>1481835484179, "update_at"=>1481835484179, "delete_at"=>0, "display_name"=>"new_team", "name"=>"new-team", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"mfgsqnmpiby18eepo6jd6pq3oh", "allow_open_invite"=>false}] +- noTeams = [] +- teams = twoTeams + +.service-installation + .inline.pull-right + = custom_icon('mattermost_logo', size: 48) + %h3 Install Mattermost Command + - if teams.count === 0 + %p + To install this service, you must be administrator of a team in the Mattermost instance at + %strong some_path.url + %p Ask your Mattermost system administrator for permissions. + %hr + .clearfix + = link_to 'Go back', 'some_url', class: 'btn btn-lg pull-right' + - else + %p + This service will be installed on the Mattermost instance at + %strong some_path.url + %hr + = form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project, )) do |f| + %h4 Team + %p Select or create the team where the slash commands will be used in + - options = mattermost_teams_options(teams) + - isOneTeam = options.count === 1 + = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if isOneTeam}", disabled: isOneTeam}) + - if isOneTeam + .help-block + This is the only team where you are an administrator. + To create a team, ask your Mattermost system administrator. + - else + .help-block + The list shows teams where you are administrator + To create a team, ask your Mattermost system administrator. + %hr + %h4 Command trigger word + %p Choose the word that will trigger commands + = f.text_field(:trigger, value: @project.path, class: 'form-control') + .help-block + %p Trigger word must be unique, and cannot begin with a slash or contain any spaces. Use the word that works best for your team. + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + %p + Reserved: + = link_to 'see list of built-in slash commands', 'some_url' + %hr + .clearfix + .pull-right + = link_to 'Cancel', 'some_url', class: 'btn btn-lg' + = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index db51c4f8a4e..66ead59fc32 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -8,14 +8,14 @@ .col-lg-9 = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = render 'shared/service_settings', form: form, subject: @service + - if @service.to_param != 'mattermost_slash_commands' + .footer-block.row-content-block + = form.submit 'Save changes', class: 'btn btn-save' +   + - if @service.valid? && @service.activated? + - unless @service.can_test? + - disabled_class = 'disabled' + - disabled_title = @service.disabled_title - .footer-block.row-content-block - = form.submit 'Save changes', class: 'btn btn-save' -   - - if @service.valid? && @service.activated? - - unless @service.can_test? - - disabled_class = 'disabled' - - disabled_title = @service.disabled_title - - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 6494d3cd793..cc19b7462da 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,6 +1,3 @@ -- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" -- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" - .well This service allows GitLab users to perform common operations on this project by entering slash commands in Mattermost. @@ -8,96 +5,5 @@ See list of available commands in Mattermost after setting up this service, by entering %code /<command_trigger_word> help - %br - %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' - on your Mattermost installation - %li - 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - %hr - - .help-form - .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#display_name') - - .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#description') - - .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - - .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#request_url') - - .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST - - .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_username') - - .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') - - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes - - .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') - - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') - - %hr - - %ul.list-unstyled - %li - 3. After adding the slash command, paste the - %strong token - into the field below - -- if Gitlab.config.mattermost.enabled - = link_to "Auto config", new_namespace_project_mattermost_path(@project.namespace, @project) += render 'projects/services/mattermost_slash_commands/installation_info' diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml new file mode 100644 index 00000000000..748660e9813 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -0,0 +1,31 @@ +-# THE ALERT BOX... +-# .alert.alert-info +-# Mattermost Command was successfully installed. You can now use GitLab inside Mattermost +-# = emoji_icon('') + +.services-installation-info + .row + %strong.col-sm-3.text-right Status + .col-sm-9= @service.activated? ? 'Installed' : 'Not installed' + .row + %strong.col-sm-3.text-right Mattermost + = link_to 'some_path.url', 'some_path.url', class: 'col-sm-9' + - if @service.activated? + .row + %strong.col-sm-3.text-right Team + .col-sm-9 some_team.name + .row + %strong.col-sm-3.text-right Installation + .col-sm-9 + - if @service.activated? + To edit or uninstall this service, press + %strong Edit in Mattermost + - else + To install this service, press + %strong Add to Mattermost + and follow the instructions + .row + .col-sm-9.col-sm-offset-3 + = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do + = custom_icon('mattermost_logo', size: 15) + = @service.activated? ? 'Edit in Mattermost' : 'Add to Mattermost' diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 9c5053dace5..12d6a112b83 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -7,44 +7,45 @@ = preserve do = markdown @service.help -.service-settings - .form-group - = form.label :active, "Active", class: "control-label" - .col-sm-10 - = form.check_box :active - - - if @service.supported_events.present? +- if @service.to_param != 'mattermost_slash_commands' + .service-settings .form-group - = form.label :url, "Trigger", class: 'control-label' - + = form.label :active, "Active", class: "control-label" .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize + = form.check_box :active - - field = @service.event_field(event) + - if @service.supported_events.present? + .form-group + = form.label :url, "Trigger", class: 'control-label' - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + .col-sm-10 + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize - %p.light - = service_event_description(event) + - field = @service.event_field(event) - - @service.global_fields.each do |field| - - type = field[:type] + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - if type == 'fieldset' - - fields = field[:fields] - - legend = field[:legend] + %p.light + = service_event_description(event) - %fieldset - %legend= legend - - fields.each do |subfield| - = render 'shared/field', form: form, field: subfield - - else - = render 'shared/field', form: form, field: field + - @service.global_fields.each do |field| + - type = field[:type] + + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] + + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb new file mode 100644 index 00000000000..83fbd1a407d --- /dev/null +++ b/app/views/shared/icons/_mattermost_logo.svg.erb @@ -0,0 +1 @@ + From 6d14a6640f89893792ad6c66b7f4362ef4ff9007 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 19 Dec 2016 14:14:09 +0100 Subject: [PATCH 017/206] Minor adjustments API Mattermost [ci skip] --- .../mattermost_slash_commands_service.rb | 4 ++-- lib/mattermost/command.rb | 13 +++---------- lib/mattermost/session.rb | 4 +++- lib/mattermost/team.rb | 17 ++++++++--------- spec/fixtures/mattermost_new_command.json | 1 - spec/lib/mattermost/command_spec.rb | 13 +++++-------- spec/lib/mattermost/session_spec.rb | 2 +- spec/lib/mattermost/team_spec.rb | 15 +++++++++------ 8 files changed, 31 insertions(+), 38 deletions(-) delete mode 100644 spec/fixtures/mattermost_new_command.json diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 5dfc4cc2744..d9fd06e4ac7 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -26,8 +26,8 @@ class MattermostSlashCommandsService < ChatService end def configure(host, current_user, params) - new_token = Mattermost::Session.new(host, current_user).with_session do - Mattermost::Command.create(params[:team_id], command) + new_token = Mattermost::Session.new(current_user).with_session do |session| + Mattermost::Command.create(session, params[:team_id], command) end update!(token: new_token, active: true) diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 9c37d0b0d79..830e61b015e 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,14 +1,7 @@ module Mattermost - class Command < Session - def self.create(team_id, trigger: 'gitlab', url:, icon_url:) - - post_command(command)['token'] - end - - private - - def post_command(command) - post( "/teams/#{team_id}/commands/create", body: command.to_json).parsed_response + class Command + def self.create(session, team_id, command) + session.post("/api/v3/teams/#{team_id}/commands/create", body: command.to_json)['token'] end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index dcdff94814c..670f83bb6bc 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -30,6 +30,8 @@ module Mattermost begin yield self + rescue Errno::ECONNREFUSED + raise NoSessionError ensure destroy end @@ -112,4 +114,4 @@ module Mattermost end end end -end \ No newline at end of file +end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 714748aea3c..9e1b22623a3 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,21 +1,20 @@ module Mattermost - class Team < Session - def self.team_admin - return [] unless initial_load['team_members'] + class Team + def self.team_admin(session) + response_body = initial_load(session) + return [] unless response_body['team_members'] - team_ids = initial_load['team_members'].map do |team| + team_ids = response_body['team_members'].map do |team| team['team_id'] if team['roles'].split.include?('team_admin') end.compact - initial_load['teams'].select do |team| + response_body['teams'].select do |team| team_ids.include?(team['id']) end end - private - - def initial_load - @initial_load ||= get('/users/initial_load').parsed_response + def self.initial_load(session) + session.get('/api/v3/users/initial_load').parsed_response end end end diff --git a/spec/fixtures/mattermost_new_command.json b/spec/fixtures/mattermost_new_command.json deleted file mode 100644 index 4b827f19926..00000000000 --- a/spec/fixtures/mattermost_new_command.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"y8j1nexrdirj5nubq5uzdwwidr","token":"pzajm5hfbtni3r49ujpt8betpc","create_at":1481897117122,"update_at":1481897117122,"delete_at":0,"creator_id":"78nm4euoc7dypergdc13ekxgpo","team_id":"w59qt5a817f69jkxdz6xe7y4ir","trigger":"display","method":"P","username":"GitLab","icon_url":"","auto_complete":false,"auto_complete_desc":"","auto_complete_hint":"","display_name":"Display name","description":"the description","url":"http://trigger.url/trigger"} diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 7c6457f639d..8c4b12c4d03 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -1,17 +1,14 @@ require 'spec_helper' describe Mattermost::Command do + let(:session) { double("session") } + describe '.create' do - let(:new_command) do - JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_new_command.json'))) - end - it 'gets the teams' do - allow(described_class).to receive(:post_command).and_return(new_command) + allow(session).to receive(:post).and_return('token' => 'token') + expect(session).to receive(:post) - token = described_class.create('abc', url: 'http://trigger.url/trigger', icon_url: 'http://myicon.com/icon.png') - - expect(token).to eq('pzajm5hfbtni3r49ujpt8betpc') + described_class.create(session, 'abc', url: 'http://trigger.com') end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 752ac796b1c..3c2eddbd221 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -96,4 +96,4 @@ describe Mattermost::Session, type: :request do end end end -end \ No newline at end of file +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 0fe6163900d..b3db2999070 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -2,20 +2,23 @@ require 'spec_helper' describe Mattermost::Team do describe '.team_admin' do - let(:init_load) do - JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json'))) - end + let(:session) { double("session") } + let(:json) { File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json')) } + let(:parsed_response) { JSON.parse(json) } before do - allow(described_class).to receive(:initial_load).and_return(init_load) + allow(session).to receive(:get).with('/api/v3/users/initial_load'). + and_return(json) + allow(json).to receive(:parsed_response).and_return(parsed_response) end it 'gets the teams' do - expect(described_class.team_admin.count).to be(2) + expect(described_class.team_admin(session).count).to be(2) end it 'filters on being team admin' do - ids = described_class.team_admin.map { |team| team['id'] } + ids = described_class.team_admin(session).map { |team| team['id'] } + expect(ids).to include("w59qt5a817f69jkxdz6xe7y4ir", "my9oujxf5jy1zqdgu9rihd66do") end end From 53aeb33f46ec8078dc4cdbdbbd509e091d7c04bf Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 14:21:39 +0000 Subject: [PATCH 018/206] Correct views for first iteration agreement --- app/helpers/projects_helper.rb | 2 +- app/views/projects/mattermost/new.html.haml | 9 +- app/views/projects/services/_form.html.haml | 19 ++-- .../_detailed_help.html.haml | 91 +++++++++++++++++++ .../mattermost_slash_commands/_help.html.haml | 5 +- app/views/shared/_service_settings.html.haml | 67 +++++++------- 6 files changed, 140 insertions(+), 53 deletions(-) create mode 100644 app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c685ec98416..db72e28445e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -153,7 +153,7 @@ module ProjectsHelper return nil unless team['display_name'] && team['id'] [team['display_name'], team['id']] end.compact - teams_options.unshift(['Select a team...', '0']) unless teams_options.count === 1 + teams_options.unshift(['Select a team...', '0']) unless teams_options.one? teams_options end diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index 6a5c9df543d..1387f5b9b10 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -1,13 +1,8 @@ -- twoTeams = [{"id"=>"w59qt5a817f69jkxdz6xe7y4ir", "create_at"=>1481835484179, "update_at"=>1481835484179, "delete_at"=>0, "display_name"=>"new_team", "name"=>"new-team", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"mfgsqnmpiby18eepo6jd6pq3oh", "allow_open_invite"=>false}, {"id"=>"my9oujxf5jy1zqdgu9rihd66do", "create_at"=>1481826062406, "update_at"=>1481826062406, "delete_at"=>0, "display_name"=>"chatops", "name"=>"chatops", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"s7c1phenmi8udkybcyytc3pxuh", "allow_open_invite"=>false}] -- oneTeams = [{"id"=>"w59qt5a817f69jkxdz6xe7y4ir", "create_at"=>1481835484179, "update_at"=>1481835484179, "delete_at"=>0, "display_name"=>"new_team", "name"=>"new-team", "email"=>"", "type"=>"O", "company_name"=>"", "allowed_domains"=>"", "invite_id"=>"mfgsqnmpiby18eepo6jd6pq3oh", "allow_open_invite"=>false}] -- noTeams = [] -- teams = twoTeams - .service-installation .inline.pull-right = custom_icon('mattermost_logo', size: 48) %h3 Install Mattermost Command - - if teams.count === 0 + - if @teams.empty? %p To install this service, you must be administrator of a team in the Mattermost instance at %strong some_path.url @@ -23,7 +18,7 @@ = form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project, )) do |f| %h4 Team %p Select or create the team where the slash commands will be used in - - options = mattermost_teams_options(teams) + - options = mattermost_teams_options(@teams) - isOneTeam = options.count === 1 = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if isOneTeam}", disabled: isOneTeam}) - if isOneTeam diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 66ead59fc32..fc338dcf887 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -8,14 +8,13 @@ .col-lg-9 = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = render 'shared/service_settings', form: form, subject: @service - - if @service.to_param != 'mattermost_slash_commands' - .footer-block.row-content-block - = form.submit 'Save changes', class: 'btn btn-save' -   - - if @service.valid? && @service.activated? - - unless @service.can_test? - - disabled_class = 'disabled' - - disabled_title = @service.disabled_title + .footer-block.row-content-block + = form.submit 'Save changes', class: 'btn btn-save' +   + - if @service.valid? && @service.activated? + - unless @service.can_test? + - disabled_class = 'disabled' + - disabled_title = @service.disabled_title - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml new file mode 100644 index 00000000000..8ca4c51a064 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -0,0 +1,91 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +To setup this service: +%ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + +%hr + +.help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + +%hr + +%ul.list-unstyled + %li + 3. After adding the slash command, paste the + + %strong token + into the field below diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index cc19b7462da..7ed291e09db 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,3 +1,5 @@ +- enabled = Gitlab.config.mattermost.enabled + .well This service allows GitLab users to perform common operations on this project by entering slash commands in Mattermost. @@ -5,5 +7,6 @@ See list of available commands in Mattermost after setting up this service, by entering %code /<command_trigger_word> help + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service unless enabled -= render 'projects/services/mattermost_slash_commands/installation_info' += render 'projects/services/mattermost_slash_commands/installation_info' if enabled diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 12d6a112b83..9c5053dace5 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -7,45 +7,44 @@ = preserve do = markdown @service.help -- if @service.to_param != 'mattermost_slash_commands' - .service-settings +.service-settings + .form-group + = form.label :active, "Active", class: "control-label" + .col-sm-10 + = form.check_box :active + + - if @service.supported_events.present? .form-group - = form.label :active, "Active", class: "control-label" + = form.label :url, "Trigger", class: 'control-label' + .col-sm-10 - = form.check_box :active + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize - - if @service.supported_events.present? - .form-group - = form.label :url, "Trigger", class: 'control-label' + - field = @service.event_field(event) - .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - field = @service.event_field(event) + %p.light + = service_event_description(event) - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + - @service.global_fields.each do |field| + - type = field[:type] - %p.light - = service_event_description(event) + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] - - @service.global_fields.each do |field| - - type = field[:type] - - - if type == 'fieldset' - - fields = field[:fields] - - legend = field[:legend] - - %fieldset - %legend= legend - - fields.each do |subfield| - = render 'shared/field', form: form, field: subfield - - else - = render 'shared/field', form: form, field: field + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field From 9ee6e5252ecb0a6a17c3d6336e8fb824153ad626 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 14:57:03 +0000 Subject: [PATCH 019/206] UX review changes --- app/helpers/projects_helper.rb | 2 +- app/views/projects/mattermost/new.html.haml | 37 ++++++++++++------- .../_installation_info.html.haml | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index db72e28445e..963e72ce96e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -153,7 +153,7 @@ module ProjectsHelper return nil unless team['display_name'] && team['id'] [team['display_name'], team['id']] end.compact - teams_options.unshift(['Select a team...', '0']) unless teams_options.one? + teams_options.unshift(['Select team...', '0']) unless teams_options.one? teams_options end diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index 1387f5b9b10..cc05fb1eeb6 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -4,31 +4,38 @@ %h3 Install Mattermost Command - if @teams.empty? %p - To install this service, you must be administrator of a team in the Mattermost instance at - %strong some_path.url - %p Ask your Mattermost system administrator for permissions. + You aren’t a member of any team on the Mattermost instance at + %strong= Gitlab.config.mattermost.host + %p + To install this service, + = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do + join a team + = icon('external-link') + and try again. %hr .clearfix - = link_to 'Go back', 'some_url', class: 'btn btn-lg pull-right' + = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' - else %p This service will be installed on the Mattermost instance at - %strong some_path.url + %strong= Gitlab.config.mattermost.host %hr - = form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project, )) do |f| + = form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| %h4 Team %p Select or create the team where the slash commands will be used in - options = mattermost_teams_options(@teams) - isOneTeam = options.count === 1 = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if isOneTeam}", disabled: isOneTeam}) - - if isOneTeam - .help-block + .help-block + - if isOneTeam This is the only team where you are an administrator. - To create a team, ask your Mattermost system administrator. - - else - .help-block + - else The list shows teams where you are administrator - To create a team, ask your Mattermost system administrator. + To create a team, ask your Mattermost system administrator. + To create a team, + = link_to "#{Gitlab.config.mattermost.host}/create_team" do + use Mattermost's interface + = icon('external-link') %hr %h4 Command trigger word %p Choose the word that will trigger commands @@ -43,9 +50,11 @@ %code= @project.path_with_namespace %p Reserved: - = link_to 'see list of built-in slash commands', 'some_url' + = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do + see list of built-in slash commands + = icon('external-link') %hr .clearfix .pull-right - = link_to 'Cancel', 'some_url', class: 'btn btn-lg' + = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 748660e9813..c0585528e47 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -9,7 +9,7 @@ .col-sm-9= @service.activated? ? 'Installed' : 'Not installed' .row %strong.col-sm-3.text-right Mattermost - = link_to 'some_path.url', 'some_path.url', class: 'col-sm-9' + = link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' - if @service.activated? .row %strong.col-sm-3.text-right Team From 6a43111f45dbbbf2f304c9b115a89ded35124626 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:02:49 +0000 Subject: [PATCH 020/206] Removed UI to show the registered team as its not yet possible --- .../mattermost_slash_commands/_installation_info.html.haml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index c0585528e47..6db028a0b8a 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -10,10 +10,6 @@ .row %strong.col-sm-3.text-right Mattermost = link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' - - if @service.activated? - .row - %strong.col-sm-3.text-right Team - .col-sm-9 some_team.name .row %strong.col-sm-3.text-right Installation .col-sm-9 From 8df4a4124d6f30c63ad93ae55a75cf37cc494a4d Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:08:16 +0000 Subject: [PATCH 021/206] Remove mock emoji comments --- .../mattermost_slash_commands/_installation_info.html.haml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 6db028a0b8a..85fd17ff2ce 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -1,8 +1,3 @@ --# THE ALERT BOX... --# .alert.alert-info --# Mattermost Command was successfully installed. You can now use GitLab inside Mattermost --# = emoji_icon('') - .services-installation-info .row %strong.col-sm-3.text-right Status From 9d7744594f7fd62ccfdb003f069596437465f7f2 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:56:39 +0000 Subject: [PATCH 022/206] zj frontend review --- app/views/projects/mattermost/new.html.haml | 5 ++--- .../mattermost_slash_commands/_installation_info.html.haml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index cc05fb1eeb6..02521579df7 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -24,10 +24,9 @@ %h4 Team %p Select or create the team where the slash commands will be used in - options = mattermost_teams_options(@teams) - - isOneTeam = options.count === 1 - = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if isOneTeam}", disabled: isOneTeam}) + = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if options.count.one?}", disabled: options.count.one?}) .help-block - - if isOneTeam + - if options.count.one? This is the only team where you are an administrator. - else The list shows teams where you are administrator diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 85fd17ff2ce..11a7bd7d30e 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -4,7 +4,7 @@ .col-sm-9= @service.activated? ? 'Installed' : 'Not installed' .row %strong.col-sm-3.text-right Mattermost - = link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' + = link_to Gitlab.config.mattermost.host.gsub(/\A.*?:\/\//, ''), Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' .row %strong.col-sm-3.text-right Installation .col-sm-9 From b67ad2db8717a0f9b673f828f9bea5732e7d258a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 16:54:14 +0000 Subject: [PATCH 023/206] Added integration tests --- .../services/mattermost_slash_command_spec.rb | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index f474e7e891b..4c08d1e6e65 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -10,23 +10,18 @@ feature 'Setup Mattermost slash commands', feature: true do before do project.team << [user, :master] login_as(user) + visit edit_namespace_project_service_path(project.namespace, project, service) end - describe 'user visites the mattermost slash command config page', js: true do + describe 'user visits the mattermost slash command config page', js: true do it 'shows a help message' do - visit edit_namespace_project_service_path(project.namespace, project, service) - wait_for_ajax expect(page).to have_content("This service allows GitLab users to perform common") end - end - - describe 'saving a token' do - let(:token) { ('a'..'z').to_a.join } it 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + token = ('a'..'z').to_a.join fill_in 'service_token', with: token click_on 'Save' @@ -35,14 +30,58 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to eq(token) end - end - describe 'the trigger url' do - it 'shows the correct url' do - visit edit_namespace_project_service_path(project.namespace, project, service) + describe 'mattermost service is enabled' do + let(:info) { find('.services-installation-info') } - value = find_field('request_url').value - expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + before do + Gitlab.config.mattermost.enabled = true + end + + it 'shows the correct mattermost url' do + expect(page).to have_content Gitlab.config.mattermost.host + end + + describe 'mattermost service is active' do + before do + service.active = true + end + + it 'shows that mattermost is active' do + expect(info).to have_content 'Installed' + expect(info).not_to have_content 'Not installed' + end + + it 'shows the edit mattermost button' do + expect(info).to have_button 'Edit Mattermost' + end + end + + describe 'mattermost service is not active' do + before do + service.active = false + end + + it 'shows that mattermost is not active' do + expect(info).to have_content 'Not installed' + end + + it 'shows the add to mattermost button' do + expect(info).to have_button 'Add to Mattermost' + end + end + end + + describe 'mattermost service is not enabled' do + before do + Gitlab.config.mattermost.enabled = false + end + + it 'shows the correct trigger url' do + value = find_field('request_url').value + + expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + end end end end From 05d04d04980db9d64c7679999200e03a6820bc31 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 17:14:03 +0000 Subject: [PATCH 024/206] kamil frontend review changes --- app/helpers/application_helper.rb | 4 ++ .../projects/mattermost/_no_teams.html.haml | 12 ++++ .../mattermost/_team_selection.html.haml | 41 ++++++++++++++ app/views/projects/mattermost/new.html.haml | 55 +------------------ .../_installation_info.html.haml | 2 +- 5 files changed, 60 insertions(+), 54 deletions(-) create mode 100644 app/views/projects/mattermost/_no_teams.html.haml create mode 100644 app/views/projects/mattermost/_team_selection.html.haml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631..adb5eeee3e4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -294,4 +294,8 @@ module ApplicationHelper def page_class "issue-boards-page" if current_controller?(:boards) end + + def pretty_url(url) + url.gsub(/\A.*?:\/\//, '') + end end diff --git a/app/views/projects/mattermost/_no_teams.html.haml b/app/views/projects/mattermost/_no_teams.html.haml new file mode 100644 index 00000000000..605c7f61dee --- /dev/null +++ b/app/views/projects/mattermost/_no_teams.html.haml @@ -0,0 +1,12 @@ +%p + You aren’t a member of any team on the Mattermost instance at + %strong= Gitlab.config.mattermost.host +%p + To install this service, + = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do + join a team + = icon('external-link') + and try again. +%hr +.clearfix + = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' diff --git a/app/views/projects/mattermost/_team_selection.html.haml b/app/views/projects/mattermost/_team_selection.html.haml new file mode 100644 index 00000000000..e0ab63dbc5d --- /dev/null +++ b/app/views/projects/mattermost/_team_selection.html.haml @@ -0,0 +1,41 @@ +%p + This service will be installed on the Mattermost instance at + %strong= Gitlab.config.mattermost.host +%hr += form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| + %h4 Team + %p Select or create the team where the slash commands will be used in + - options = mattermost_teams_options(@teams) + = f.select(:team_id, options, {}, { class: 'form-control', selected: "#{options.first[1] if options.count.one?}", disabled: options.count.one? }) + .help-block + - if options.count.one? + This is the only team where you are an administrator. + - else + The list shows teams where you are administrator + To create a team, ask your Mattermost system administrator. + To create a team, + = link_to "#{Gitlab.config.mattermost.host}/create_team" do + use Mattermost's interface + = icon('external-link') + %hr + %h4 Command trigger word + %p Choose the word that will trigger commands + = f.text_field(:trigger, value: @project.path, class: 'form-control') + .help-block + %p Trigger word must be unique, and cannot begin with a slash or contain any spaces. Use the word that works best for your team. + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + %p + Reserved: + = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do + see list of built-in slash commands + = icon('external-link') + %hr + .clearfix + .pull-right + = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' + = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermost/new.html.haml index 02521579df7..96b1d2aee61 100644 --- a/app/views/projects/mattermost/new.html.haml +++ b/app/views/projects/mattermost/new.html.haml @@ -3,57 +3,6 @@ = custom_icon('mattermost_logo', size: 48) %h3 Install Mattermost Command - if @teams.empty? - %p - You aren’t a member of any team on the Mattermost instance at - %strong= Gitlab.config.mattermost.host - %p - To install this service, - = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do - join a team - = icon('external-link') - and try again. - %hr - .clearfix - = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' + = render 'no_teams' - else - %p - This service will be installed on the Mattermost instance at - %strong= Gitlab.config.mattermost.host - %hr - = form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| - %h4 Team - %p Select or create the team where the slash commands will be used in - - options = mattermost_teams_options(@teams) - = f.select(:team_id, options, {}, {class: 'form-control', selected: "#{options.first[1] if options.count.one?}", disabled: options.count.one?}) - .help-block - - if options.count.one? - This is the only team where you are an administrator. - - else - The list shows teams where you are administrator - To create a team, ask your Mattermost system administrator. - To create a team, - = link_to "#{Gitlab.config.mattermost.host}/create_team" do - use Mattermost's interface - = icon('external-link') - %hr - %h4 Command trigger word - %p Choose the word that will trigger commands - = f.text_field(:trigger, value: @project.path, class: 'form-control') - .help-block - %p Trigger word must be unique, and cannot begin with a slash or contain any spaces. Use the word that works best for your team. - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - %p - Reserved: - = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do - see list of built-in slash commands - = icon('external-link') - %hr - .clearfix - .pull-right - = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' - = f.submit 'Install', class: 'btn btn-save btn-lg' + = render 'team_selection' diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 11a7bd7d30e..abc68e955e7 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -4,7 +4,7 @@ .col-sm-9= @service.activated? ? 'Installed' : 'Not installed' .row %strong.col-sm-3.text-right Mattermost - = link_to Gitlab.config.mattermost.host.gsub(/\A.*?:\/\//, ''), Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' + = link_to pretty_url(Gitlab.config.mattermost.host), Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' .row %strong.col-sm-3.text-right Installation .col-sm-9 From d21535602b30316646772b1cd74d7069254076df Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 19 Dec 2016 14:14:09 +0100 Subject: [PATCH 025/206] Minor adjustments API Mattermost [ci skip] --- .../projects/mattermost_controller.rb | 19 ++++----- .../mattermost_slash_commands_service.rb | 25 ++++++++++-- config/routes/project.rb | 6 +-- lib/mattermost/command.rb | 17 ++++---- lib/mattermost/session.rb | 4 +- lib/mattermost/team.rb | 20 +++------- spec/fixtures/mattermost_new_command.json | 1 - spec/lib/mattermost/command_spec.rb | 20 +++++++--- spec/lib/mattermost/session_spec.rb | 2 +- spec/lib/mattermost/team_spec.rb | 18 +++++---- .../mattermost_slash_commands_service_spec.rb | 39 +++++++++++-------- 11 files changed, 93 insertions(+), 78 deletions(-) delete mode 100644 spec/fixtures/mattermost_new_command.json diff --git a/app/controllers/projects/mattermost_controller.rb b/app/controllers/projects/mattermost_controller.rb index 1759d21e84f..a0eaec262ee 100644 --- a/app/controllers/projects/mattermost_controller.rb +++ b/app/controllers/projects/mattermost_controller.rb @@ -7,12 +7,13 @@ class Projects::MattermostController < Projects::ApplicationController def new end - def configure - @service.configure(host, current_user, configure_params) + def create + message = @service.configure(current_user, configure_params) + notice = message.is_a?(String) ? message : 'This service is now configured' redirect_to( new_namespace_project_mattermost_path(@project.namespace, @project), - notice: 'This service is now configured.' + notice: notice ) rescue NoSessionError redirect_to( @@ -24,7 +25,8 @@ class Projects::MattermostController < Projects::ApplicationController private def configure_params - params.permit(:trigger, :team_id).merge(url: service_trigger_url(@service), icon_url: asset_url('gitlab_logo.png')) + params.permit(:trigger, :team_id). + merge(url: service_trigger_url(@service), icon_url: asset_url('gitlab_logo.png')) end def service @@ -32,13 +34,6 @@ class Projects::MattermostController < Projects::ApplicationController end def teams - @teams = - begin - Mattermost::Mattermost.new(Gitlab.config.mattermost.host, current_user).with_session do - Mattermost::Team.team_admin - end - rescue - [] - end + @teams = @service.list_teams(current_user) end end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 5dfc4cc2744..6fdcb770593 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,12 +25,29 @@ class MattermostSlashCommandsService < ChatService ] end - def configure(host, current_user, params) - new_token = Mattermost::Session.new(host, current_user).with_session do - Mattermost::Command.create(params[:team_id], command) + def configure(current_user, params) + result = Mattermost::Session.new(current_user).with_session do |session| + Mattermost::Command.create(session, params[:team_id], command) end - update!(token: new_token, active: true) + if result.has_key?('message') + result['message'] + else + update!(token: result['token'], active: true) + end + end + + def list_teams + begin + response = Mattermost::Mattermost.new(current_user).with_session do |session| + Mattermost::Team.teams(session) + end + + # We ignore the error message as we can't display it + response.has_key?('message') ? [] : response + rescue Mattermost::NoSessionError + [] + end end def trigger(params) diff --git a/config/routes/project.rb b/config/routes/project.rb index 23d85368f1b..b42c5e5211c 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -76,11 +76,7 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :mattermost, only: [:new] do - collection do - post :configure - end - end + resources :mattermost, only: [:new, :create] resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 9c37d0b0d79..afbf2ce3349 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,14 +1,13 @@ module Mattermost - class Command < Session - def self.create(team_id, trigger: 'gitlab', url:, icon_url:) + class Command + def self.create(session, team_id, command) + response = session.post("/api/v3/teams/#{team_id}/commands/create", body: command.to_json).parsed_response - post_command(command)['token'] - end - - private - - def post_command(command) - post( "/teams/#{team_id}/commands/create", body: command.to_json).parsed_response + if response.has_key?('message') + response + else + response['token'] + end end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index dcdff94814c..670f83bb6bc 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -30,6 +30,8 @@ module Mattermost begin yield self + rescue Errno::ECONNREFUSED + raise NoSessionError ensure destroy end @@ -112,4 +114,4 @@ module Mattermost end end end -end \ No newline at end of file +end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 714748aea3c..c1b867629b6 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,21 +1,13 @@ module Mattermost - class Team < Session - def self.team_admin - return [] unless initial_load['team_members'] + class Team + def self.all(session) + response_body = retreive_teams(session) - team_ids = initial_load['team_members'].map do |team| - team['team_id'] if team['roles'].split.include?('team_admin') - end.compact - - initial_load['teams'].select do |team| - team_ids.include?(team['id']) - end + response_body.has_key?('message') ? response_body : response_body.values end - private - - def initial_load - @initial_load ||= get('/users/initial_load').parsed_response + def self.retreive_teams(session) + session.get('/api/v3/teams/all').parsed_response end end end diff --git a/spec/fixtures/mattermost_new_command.json b/spec/fixtures/mattermost_new_command.json deleted file mode 100644 index 4b827f19926..00000000000 --- a/spec/fixtures/mattermost_new_command.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"y8j1nexrdirj5nubq5uzdwwidr","token":"pzajm5hfbtni3r49ujpt8betpc","create_at":1481897117122,"update_at":1481897117122,"delete_at":0,"creator_id":"78nm4euoc7dypergdc13ekxgpo","team_id":"w59qt5a817f69jkxdz6xe7y4ir","trigger":"display","method":"P","username":"GitLab","icon_url":"","auto_complete":false,"auto_complete_desc":"","auto_complete_hint":"","display_name":"Display name","description":"the description","url":"http://trigger.url/trigger"} diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 7c6457f639d..bc2e47ebbc9 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -1,17 +1,25 @@ require 'spec_helper' describe Mattermost::Command do + let(:session) { double("session") } + let(:hash) { { 'token' => 'token' } } + describe '.create' do - let(:new_command) do - JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_new_command.json'))) + before do + allow(session).to receive(:post).and_return(hash) + allow(hash).to receive(:parsed_response).and_return(hash) end - it 'gets the teams' do - allow(described_class).to receive(:post_command).and_return(new_command) + context 'with access' do + it 'gets the teams' do + expect(session).to receive(:post) - token = described_class.create('abc', url: 'http://trigger.url/trigger', icon_url: 'http://myicon.com/icon.png') + described_class.create(session, 'abc', url: 'http://trigger.com') + end + end + + context 'on an error' do - expect(token).to eq('pzajm5hfbtni3r49ujpt8betpc') end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 752ac796b1c..3c2eddbd221 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -96,4 +96,4 @@ describe Mattermost::Session, type: :request do end end end -end \ No newline at end of file +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 0fe6163900d..32a0dbf42ec 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -2,20 +2,22 @@ require 'spec_helper' describe Mattermost::Team do describe '.team_admin' do - let(:init_load) do - JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json'))) - end + let(:session) { double("session") } + # TODO fix fixture + let(:json) { File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json')) } + let(:parsed_response) { JSON.parse(json) } before do - allow(described_class).to receive(:initial_load).and_return(init_load) + allow(session).to receive(:get).with('/api/v3/teams/all'). + and_return(json) + allow(json).to receive(:parsed_response).and_return(parsed_response) end - it 'gets the teams' do - expect(described_class.team_admin.count).to be(2) + xit 'gets the teams' do + expect(described_class.all(session).count).to be(2) end - it 'filters on being team admin' do - ids = described_class.team_admin.map { |team| team['id'] } + xit 'filters on being team admin' do expect(ids).to include("w59qt5a817f69jkxdz6xe7y4ir", "my9oujxf5jy1zqdgu9rihd66do") end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 43b2c2c1302..00018624d96 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -102,29 +102,34 @@ describe MattermostSlashCommandsService, models: true do let(:service) { project.build_mattermost_slash_commands_service } subject do - service.configure('http://localhost:8065', nil, team_id: 'abc', trigger: 'gitlab', url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png') + service.configure('http://localhost:8065', team_id: 'abc', trigger: 'gitlab', url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png') end - it 'creates a new Mattermost session' do - expect_any_instance_of(Mattermost::Session).to receive(:with_session) + context 'the requests succeeds' do + before do + allow_any_instance_of(Mattermost::Session).to receive(:with_session). + and_return('token' => 'mynewtoken') + end - subject + it 'saves the service' do + expect_any_instance_of(Mattermost::Session).to receive(:with_session) + expect { subject }.to change { project.services.count }.by(1) + end + + it 'saves the token' do + subject + + expect(service.reload.token).to eq('mynewtoken') + end end - it 'saves the service' do - allow_any_instance_of(Mattermost::Session).to receive(:with_session). - and_return('mynewtoken') + context 'an error is received' do + it 'shows error messages' do + allow_any_instance_of(Mattermost::Session).to receive(:with_session). + and_return('token' => 'mynewtoken', 'message' => "Error") - expect { subject }.to change { project.services.count }.by(1) - end - - it 'saves the token' do - allow_any_instance_of(Mattermost::Session).to receive(:with_session). - and_return('mynewtoken') - - subject - - expect(service.reload.token).to eq('mynewtoken') + expect(subject).to eq("Error") + end end end end From 921f411a41d92ff6b3fdea2560adbd861d97be57 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 19 Dec 2016 23:50:42 +0100 Subject: [PATCH 026/206] Last fixes --- .../mattermost_slash_commands_service.rb | 2 +- lib/mattermost/team.rb | 4 ---- spec/lib/mattermost/team_spec.rb | 24 +++++++++---------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 5000f96e350..92e2ae637fb 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -37,7 +37,7 @@ class MattermostSlashCommandsService < ChatService end end - def list_teams + def list_teams(current_user) begin response = Mattermost::Session.new(current_user).with_session do |session| Mattermost::Team.all(session) diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index ea5cfd2cb0b..5ee77aa9adf 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,10 +1,6 @@ module Mattermost class Team def self.all(session) - retreive_teams(session) - end - - def self.retreive_teams(session) session.get('/api/v3/teams/all').parsed_response end end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index d4fe63fcd8b..c208be3912b 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -6,18 +6,18 @@ describe Mattermost::Team do let(:response) do [{ - "id"=>"xiyro8huptfhdndadpz8r3wnbo", - "create_at"=>1482174222155, - "update_at"=>1482174222155, - "delete_at"=>0, - "display_name"=>"chatops", - "name"=>"chatops", - "email"=>"admin@example.com", - "type"=>"O", - "company_name"=>"", - "allowed_domains"=>"", - "invite_id"=>"o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite"=>false}] + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] end let(:json) { nil } From 34295036e2a9ecf18ca5440a5dd6dbb0c7f05643 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 23:53:19 +0100 Subject: [PATCH 027/206] Improve sources - Add proper error handling, - Use flash[:alert] and flash[:notice], - Use `resource` instead of `resources`, --- .../projects/mattermost_controller.rb | 39 ------------------ .../projects/mattermosts_controller.rb | 41 +++++++++++++++++++ app/helpers/projects_helper.rb | 9 ++-- .../mattermost_slash_commands_service.rb | 35 +++++----------- .../_no_teams.html.haml | 0 .../_team_selection.html.haml | 10 ++--- .../{mattermost => mattermosts}/new.html.haml | 0 .../_installation_info.html.haml | 27 ++++++------ config/routes/project.rb | 2 +- lib/mattermost/command.rb | 13 +++--- lib/mattermost/session.rb | 7 +++- lib/mattermost/team.rb | 10 ++++- 12 files changed, 96 insertions(+), 97 deletions(-) delete mode 100644 app/controllers/projects/mattermost_controller.rb create mode 100644 app/controllers/projects/mattermosts_controller.rb rename app/views/projects/{mattermost => mattermosts}/_no_teams.html.haml (100%) rename app/views/projects/{mattermost => mattermosts}/_team_selection.html.haml (78%) rename app/views/projects/{mattermost => mattermosts}/new.html.haml (100%) diff --git a/app/controllers/projects/mattermost_controller.rb b/app/controllers/projects/mattermost_controller.rb deleted file mode 100644 index a0eaec262ee..00000000000 --- a/app/controllers/projects/mattermost_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -class Projects::MattermostController < Projects::ApplicationController - layout 'project_settings' - before_action :authorize_admin_project! - before_action :service - before_action :teams, only: [:new] - - def new - end - - def create - message = @service.configure(current_user, configure_params) - notice = message.is_a?(String) ? message : 'This service is now configured' - - redirect_to( - new_namespace_project_mattermost_path(@project.namespace, @project), - notice: notice - ) - rescue NoSessionError - redirect_to( - new_namespace_project_mattermost_path(@project.namespace, @project), - alert: 'No session could be set up, is Mattermost configured with Single Sign on?' - ) - end - - private - - def configure_params - params.permit(:trigger, :team_id). - merge(url: service_trigger_url(@service), icon_url: asset_url('gitlab_logo.png')) - end - - def service - @service ||= @project.find_or_initialize_service('mattermost_slash_commands') - end - - def teams - @teams = @service.list_teams(current_user) - end -end diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb new file mode 100644 index 00000000000..c6b39add7ad --- /dev/null +++ b/app/controllers/projects/mattermosts_controller.rb @@ -0,0 +1,41 @@ +class Projects::MattermostsController < Projects::ApplicationController + include TriggersHelper + include ActionView::Helpers::AssetUrlHelper + + layout 'project_settings' + + before_action :authorize_admin_project! + before_action :service + before_action :teams, only: [:new] + + def new + end + + def create + @service.configure!(current_user, configure_params) + + flash[:notice] = 'This service is now configured' + redirect_to edit_namespace_project_service_path(@project.namespace, @project, service) + rescue => e + flash[:alert] = e.message + redirect_to new_namespace_project_mattermost_path(@project.namespace, @project) + end + + private + + def configure_params + params.require(:mattermost).permit(:trigger, :team_id).merge( + url: service_trigger_url(@service), + icon_url: asset_url('gitlab_logo.png')) + end + + def teams + @teams ||= @service.list_teams(current_user) + rescue => e + flash[:alert] = e.message + end + + def service + @service ||= @project.find_or_initialize_service('mattermost_slash_commands') + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 963e72ce96e..b7731ab4be2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -149,12 +149,11 @@ module ProjectsHelper end def mattermost_teams_options(teams) - teams_options = teams.map do |team| - return nil unless team['display_name'] && team['id'] - [team['display_name'], team['id']] + teams_options = teams.map do |id, options| + return nil unless id && options['display_name'] + [options['display_name'], id] end.compact - teams_options.unshift(['Select team...', '0']) unless teams_options.one? - teams_options + teams_options.unshift(['Select team...', '0']) end private diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 92e2ae637fb..51de80f38de 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,28 +25,17 @@ class MattermostSlashCommandsService < ChatService ] end - def configure(current_user, params) - result = Mattermost::Session.new(current_user).with_session do |session| - Mattermost::Command.create(session, params[:team_id], command) + def configure!(current_user, params) + token = Mattermost::Session.new(current_user).with_session do |session| + Mattermost::Command.create(session, command(params)) end - if result.has_key?('message') - result['message'] - else - update!(token: result['token'], active: true) - end + update!(active: true, token: token) end - def list_teams(current_user) - begin - response = Mattermost::Session.new(current_user).with_session do |session| - Mattermost::Team.all(session) - end - - # We ignore the error message as we can't display it - response.has_key?('message') ? [] : response - rescue Mattermost::NoSessionError - [] + def list_teams(user) + Mattermost::Session.new(user).with_session do |session| + Mattermost::Team.all(session) end end @@ -64,21 +53,17 @@ class MattermostSlashCommandsService < ChatService private - def command(trigger:, url:, icon_url:) + def command(params) pretty_project_name = project.name_with_namespace - { + params.merge( auto_complete: true, auto_complete_desc: "Perform common operations on: #{pretty_project_name}", auto_complete_hint: '[help]', description: "Perform common operations on: #{pretty_project_name}", display_name: "GitLab / #{pretty_project_name}", method: 'P', - user_name: 'GitLab', - trigger: trigger, - url: url, - icon_url: icon_url - } + user_name: 'GitLab') end def find_chat_user(params) diff --git a/app/views/projects/mattermost/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml similarity index 100% rename from app/views/projects/mattermost/_no_teams.html.haml rename to app/views/projects/mattermosts/_no_teams.html.haml diff --git a/app/views/projects/mattermost/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml similarity index 78% rename from app/views/projects/mattermost/_team_selection.html.haml rename to app/views/projects/mattermosts/_team_selection.html.haml index e0ab63dbc5d..376592e66c9 100644 --- a/app/views/projects/mattermost/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -1,14 +1,14 @@ %p This service will be installed on the Mattermost instance at - %strong= Gitlab.config.mattermost.host + %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host %hr -= form_for(:create, method: :post, url: configure_namespace_project_mattermost_index_path(@project.namespace, @project)) do |f| += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| %h4 Team %p Select or create the team where the slash commands will be used in - - options = mattermost_teams_options(@teams) - = f.select(:team_id, options, {}, { class: 'form-control', selected: "#{options.first[1] if options.count.one?}", disabled: options.count.one? }) + - selected_id = @teams.keys.first if @teams.one? + = f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? }) .help-block - - if options.count.one? + - if @teams.one? This is the only team where you are an administrator. - else The list shows teams where you are administrator diff --git a/app/views/projects/mattermost/new.html.haml b/app/views/projects/mattermosts/new.html.haml similarity index 100% rename from app/views/projects/mattermost/new.html.haml rename to app/views/projects/mattermosts/new.html.haml diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index abc68e955e7..e6fcb09e054 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -5,18 +5,15 @@ .row %strong.col-sm-3.text-right Mattermost = link_to pretty_url(Gitlab.config.mattermost.host), Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' - .row - %strong.col-sm-3.text-right Installation - .col-sm-9 - - if @service.activated? - To edit or uninstall this service, press - %strong Edit in Mattermost - - else - To install this service, press - %strong Add to Mattermost - and follow the instructions - .row - .col-sm-9.col-sm-offset-3 - = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do - = custom_icon('mattermost_logo', size: 15) - = @service.activated? ? 'Edit in Mattermost' : 'Add to Mattermost' + - unless @service.activated? + .row + %strong.col-sm-3.text-right Installation + .col-sm-9 + To install this service, press + %strong Add to Mattermost + and follow the instructions + .row + .col-sm-9.col-sm-offset-3 + = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do + = custom_icon('mattermost_logo', size: 15) + = 'Add to Mattermost' diff --git a/config/routes/project.rb b/config/routes/project.rb index b42c5e5211c..1d0caac3080 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -76,7 +76,7 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :mattermost, only: [:new, :create] + resource :mattermost, only: [:new, :create] resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index afbf2ce3349..5c6f5861a7f 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,12 +1,15 @@ module Mattermost class Command - def self.create(session, team_id, command) - response = session.post("/api/v3/teams/#{team_id}/commands/create", body: command.to_json).parsed_response + def self.create(session, params) + response = session.post("/api/v3/teams/#{params[:team_id]}/commands/create", + body: params.to_json) - if response.has_key?('message') - response + if response.success? + response.parsed_response['token'] + elsif response.parsed_response.try(:has_key?, 'message') + raise response.parsed_response['message'] else - response['token'] + raise 'Failed to create a new command' end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 670f83bb6bc..f0ce51d6a71 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,5 +1,10 @@ module Mattermost - class NoSessionError < StandardError; end + class NoSessionError < StandardError + def message + 'No session could be set up, is Mattermost configured with Single Sign on?' + end + end + # This class' prime objective is to obtain a session token on a Mattermost # instance with SSO configured where this GitLab instance is the provider. # diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 5ee77aa9adf..2de01eced0b 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,15 @@ module Mattermost class Team def self.all(session) - session.get('/api/v3/teams/all').parsed_response + response = session.get('/api/v3/teams/all') + + if response.success? + response.parsed_response + elsif response.parsed_response.try(:has_key?, 'message') + raise response.parsed_response['message'] + else + raise 'Failed to list teams' + end end end end From 841960f847f04da9c427bcdb19037e2112a90890 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 00:22:10 +0100 Subject: [PATCH 028/206] Fix flow --- .../projects/mattermosts_controller.rb | 1 + .../mattermost_slash_commands_service.rb | 11 ++---- lib/mattermost/client.rb | 39 +++++++++++++++++++ lib/mattermost/command.rb | 14 ++----- lib/mattermost/team.rb | 14 ++----- 5 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 lib/mattermost/client.rb diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index c6b39add7ad..0f939838306 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -32,6 +32,7 @@ class Projects::MattermostsController < Projects::ApplicationController def teams @teams ||= @service.list_teams(current_user) rescue => e + @teams = [] flash[:alert] = e.message end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 51de80f38de..accf59bea18 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -25,18 +25,15 @@ class MattermostSlashCommandsService < ChatService ] end - def configure!(current_user, params) - token = Mattermost::Session.new(current_user).with_session do |session| - Mattermost::Command.create(session, command(params)) - end + def configure!(user, params) + token = Mattermost::Command.new(user). + create(command(params)) update!(active: true, token: token) end def list_teams(user) - Mattermost::Session.new(user).with_session do |session| - Mattermost::Team.all(session) - end + Mattermost::Team.new(user).all end def trigger(params) diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb new file mode 100644 index 00000000000..2a0e4b400d3 --- /dev/null +++ b/lib/mattermost/client.rb @@ -0,0 +1,39 @@ +module Mattermost + class Client + attr_reader :user + + def initialize(user) + @user = user + end + + private + + def with_session(&blk) + Session.new(user).with_session(&blk) + end + + def json_get(path, options = {}) + with_session do |session| + json_response session.get(path, options) + end + end + + def json_post(path, options = {}) + with_session do |session| + json_response session.post(path, options) + end + end + + def json_response(response) + json_response = JSON.parse(response.body) + + if response.success? + json_response + elsif json_response['message'] + raise json_response['message'] + else + raise 'Undefined error' + end + end + end +end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 5c6f5861a7f..d1e4bb0eccf 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,16 +1,10 @@ module Mattermost - class Command - def self.create(session, params) - response = session.post("/api/v3/teams/#{params[:team_id]}/commands/create", + class Command < Client + def create(params) + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", body: params.to_json) - if response.success? - response.parsed_response['token'] - elsif response.parsed_response.try(:has_key?, 'message') - raise response.parsed_response['message'] - else - raise 'Failed to create a new command' - end + response['token'] end end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 2de01eced0b..784eca6ab5a 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,15 +1,7 @@ module Mattermost - class Team - def self.all(session) - response = session.get('/api/v3/teams/all') - - if response.success? - response.parsed_response - elsif response.parsed_response.try(:has_key?, 'message') - raise response.parsed_response['message'] - else - raise 'Failed to list teams' - end + class Team < Client + def all + json_get('/api/v3/teams/all') end end end From f7b7e918fef6567d26e7fe17894e5df14c58f37c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 00:29:38 +0100 Subject: [PATCH 029/206] Remove rest of the form parameters as we can't really support them --- .../mattermost_slash_commands/_help.html.haml | 7 +++++-- .../_installation_info.html.haml | 12 ------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 7ed291e09db..63b797cd391 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -7,6 +7,9 @@ See list of available commands in Mattermost after setting up this service, by entering %code /<command_trigger_word> help - = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service unless enabled -= render 'projects/services/mattermost_slash_commands/installation_info' if enabled + - unless enabled + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service + +- if enabled + = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index e6fcb09e054..c929eee3bb9 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -1,17 +1,5 @@ .services-installation-info - .row - %strong.col-sm-3.text-right Status - .col-sm-9= @service.activated? ? 'Installed' : 'Not installed' - .row - %strong.col-sm-3.text-right Mattermost - = link_to pretty_url(Gitlab.config.mattermost.host), Gitlab.config.mattermost.host, class: 'col-sm-9', target: '__blank' - unless @service.activated? - .row - %strong.col-sm-3.text-right Installation - .col-sm-9 - To install this service, press - %strong Add to Mattermost - and follow the instructions .row .col-sm-9.col-sm-offset-3 = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do From 4e93cb9c2f89387c02bc59e8fdf0d5a93c516f7a Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Thu, 15 Dec 2016 13:29:51 +0900 Subject: [PATCH 030/206] Cache last commit sha for path --- app/models/repository.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 1ccabdb7c1f..bedb3be88c5 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -654,11 +654,22 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - sha = Gitlab::Popen.popen(args, path_to_repo).first.strip + sha = cache_last_commit_sha_for_path(sha, path) commit(sha) end + def last_commit_sha_for_path(sha, path) + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) + Gitlab::Popen.popen(args, path_to_repo).first.strip + end + + def cache_last_commit_sha_for_path(sha, path) + key = path.blank? ? "last_commit_sha_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" + cache.fetch(key) do + last_commit_sha_for_path(sha, path) + end + end + def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name From b1ca2c7dd8c54d0c430eee3882fa10f8e747add3 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Thu, 15 Dec 2016 14:21:12 +0900 Subject: [PATCH 031/206] Use Repository#cache_last_commit_sha_for_path in API --- lib/api/files.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/files.rb b/lib/api/files.rb index 28f306e45f3..187e2f3434d 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -70,7 +70,7 @@ module API ref: params[:ref], blob_id: blob.id, commit_id: commit.id, - last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id + last_commit_id: repo.cache_last_commit_sha_for_path(commit.sha, params[:file_path]) } end From 1f1927571990ab44c7a45fbc436438756189d783 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Thu, 15 Dec 2016 14:40:15 +0900 Subject: [PATCH 032/206] Rename sha to id --- app/models/repository.rb | 10 +++++----- lib/api/files.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index bedb3be88c5..58eab25b283 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -654,19 +654,19 @@ class Repository end def last_commit_for_path(sha, path) - sha = cache_last_commit_sha_for_path(sha, path) + sha = cache_last_commit_id_for_path(sha, path) commit(sha) end - def last_commit_sha_for_path(sha, path) + def last_commit_id_for_path(sha, path) args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) Gitlab::Popen.popen(args, path_to_repo).first.strip end - def cache_last_commit_sha_for_path(sha, path) - key = path.blank? ? "last_commit_sha_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" + def cache_last_commit_id_for_path(sha, path) + key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - last_commit_sha_for_path(sha, path) + last_commit_id_for_path(sha, path) end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 187e2f3434d..f95b9be1722 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -70,7 +70,7 @@ module API ref: params[:ref], blob_id: blob.id, commit_id: commit.id, - last_commit_id: repo.cache_last_commit_sha_for_path(commit.sha, params[:file_path]) + last_commit_id: repo.cache_last_commit_id_for_path(commit.sha, params[:file_path]) } end From 1da8bd8f049d0803ce8552f22ddf7a08206c9fb6 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 16 Dec 2016 14:02:00 +0900 Subject: [PATCH 033/206] Add specs. --- spec/models/repository_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b5a42edd192..9c4ce13e223 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -139,6 +139,27 @@ describe Repository, models: true do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end + describe '#last_commit_id_for_path' do + subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } + + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + end + + describe '#cache_last_commit_id_for_path' do + subject { repository.cache_last_commit_id_for_path(sample_commit.id, '.gitignore') } + let(:cache) { repository.send(:cache) } + let(:key) { "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.digest('.gitignore')}" } + before { cache.expire(key) } + after { cache.expire(key) } + + it "caches #last_commit_id_for_path" do + expect(repository).to receive(:last_commit_id_for_path).once.and_return('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + 2.times do + is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + end + end + end + describe '#find_commits_by_message' do it 'returns commits with messages containing a given string' do commit_ids = repository.find_commits_by_message('submodule').map(&:id) From 16511728cabb0284b9bd0011163a6d7b4acf1549 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 16 Dec 2016 16:34:28 +0900 Subject: [PATCH 034/206] Add changelog entry. --- changelogs/unreleased/cache-last-commit-sha-for-path.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/cache-last-commit-sha-for-path.yml diff --git a/changelogs/unreleased/cache-last-commit-sha-for-path.yml b/changelogs/unreleased/cache-last-commit-sha-for-path.yml new file mode 100644 index 00000000000..9cd8c5bab86 --- /dev/null +++ b/changelogs/unreleased/cache-last-commit-sha-for-path.yml @@ -0,0 +1,4 @@ +--- +title: Cache last commit id for path +merge_request: 8098 +author: Hiroyuki Sato From b6d4c88d332c80992b880894feb4c4370e8f7b88 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Tue, 20 Dec 2016 00:45:51 +0900 Subject: [PATCH 035/206] Clean-up spec --- spec/models/repository_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9c4ce13e223..5e38f8bec16 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -146,17 +146,11 @@ describe Repository, models: true do end describe '#cache_last_commit_id_for_path' do - subject { repository.cache_last_commit_id_for_path(sample_commit.id, '.gitignore') } - let(:cache) { repository.send(:cache) } - let(:key) { "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.digest('.gitignore')}" } - before { cache.expire(key) } - after { cache.expire(key) } - it "caches #last_commit_id_for_path" do - expect(repository).to receive(:last_commit_id_for_path).once.and_return('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') - 2.times do - is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') - end + cache = repository.send(:cache) + key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') + expect(repository.cache_last_commit_id_for_path(sample_commit.id, '.gitignore')).to eq('c1acaa5') end end From d305d15b6dde99b0de7fc78242aaa095fc79b5ca Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 11:32:42 +0100 Subject: [PATCH 036/206] Fix Mattermost client --- lib/mattermost/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index 2a0e4b400d3..8bbb2038772 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -9,7 +9,7 @@ module Mattermost private def with_session(&blk) - Session.new(user).with_session(&blk) + Mattermost::Session.new(user).with_session(&blk) end def json_get(path, options = {}) From 0cf23fde7c666b64e6c18a92d29e632f51b00059 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 12:02:37 +0100 Subject: [PATCH 037/206] Work on tests for mattermost --- app/helpers/projects_helper.rb | 8 ++--- lib/mattermost/client.rb | 6 ++-- lib/mattermost/session.rb | 14 +++++++- .../mattermost_slash_commands_service_spec.rb | 35 +++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b7731ab4be2..bd2dcb08b3e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -150,10 +150,10 @@ module ProjectsHelper def mattermost_teams_options(teams) teams_options = teams.map do |id, options| - return nil unless id && options['display_name'] - [options['display_name'], id] - end.compact - teams_options.unshift(['Select team...', '0']) + [options['display_name'] || options['name'], id] + end + + teams_options.compact.unshift(['Select team...', '0']) end private diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index 8bbb2038772..d6759eac0d0 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -1,4 +1,6 @@ module Mattermost + class ClientError < Mattermost::Error; end + class Client attr_reader :user @@ -30,9 +32,9 @@ module Mattermost if response.success? json_response elsif json_response['message'] - raise json_response['message'] + raise ClientError(json_response['message']) else - raise 'Undefined error' + raise ClientError('Undefined error') end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index f0ce51d6a71..e36500d24a3 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,10 +1,18 @@ module Mattermost - class NoSessionError < StandardError + class Error < StandardError; end + + class NoSessionError < Error def message 'No session could be set up, is Mattermost configured with Single Sign on?' end end + class ConnectionError < Error + def message + 'Could not connect. Is Mattermost up?' + end + end + # This class' prime objective is to obtain a session token on a Mattermost # instance with SSO configured where this GitLab instance is the provider. # @@ -66,10 +74,14 @@ module Mattermost def get(path, options = {}) self.class.get(path, options.merge(headers: @headers)) + rescue Errno::ECONNREFUSED + raise ConnectionError end def post(path, options = {}) self.class.post(path, options.merge(headers: @headers)) + rescue Errno::ECONNREFUSED + raise ConnectionError end private diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 1ae1483e2a4..9fb6132d171 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -2,4 +2,39 @@ require 'spec_helper' describe MattermostSlashCommandsService, :models do it_behaves_like "chat slash commands service" + + describe '#configure!' do + let(:project) { create(:empty_project) } + let(:service) { project.build_mattermost_slash_commands_service } + let(:user) { create(:user)} + + before do + allow_any_instance_of(Mattermost::Session).to + receive(:with_session).and_yield + end + + subject do + service.configure!(user, team_id: 'abc', + trigger: 'gitlab', url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png') + end + + context 'the requests succeeds' do + it 'saves the service' do + expect { subject }.to change { project.services.count }.by(1) + end + + it 'saves the token' do + subject + + expect(service.reload.token).to eq('mynewtoken') + end + end + + context 'an error is received' do + it 'shows error messages' do + expect(subject).to raise_error("Error") + end + end + end end From e78f1048d9f9e327f6386dbeae020a2217d8e05f Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Tue, 20 Dec 2016 20:57:21 +0900 Subject: [PATCH 038/206] Add a blank line before first expect. --- spec/models/repository_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5e38f8bec16..9c66bbdb5f3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -149,6 +149,7 @@ describe Repository, models: true do it "caches #last_commit_id_for_path" do cache = repository.send(:cache) key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') expect(repository.cache_last_commit_id_for_path(sample_commit.id, '.gitignore')).to eq('c1acaa5') end From 1a59766d0c7ed78a7167040af512842979d2414e Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Tue, 20 Dec 2016 22:43:59 +0900 Subject: [PATCH 039/206] Merge two methods. --- app/models/repository.rb | 11 ++++------- lib/api/files.rb | 2 +- spec/models/repository_spec.rb | 10 +++++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 58eab25b283..3266e9c75f0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -654,19 +654,16 @@ class Repository end def last_commit_for_path(sha, path) - sha = cache_last_commit_id_for_path(sha, path) + sha = last_commit_id_for_path(sha, path) commit(sha) end def last_commit_id_for_path(sha, path) - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - Gitlab::Popen.popen(args, path_to_repo).first.strip - end - - def cache_last_commit_id_for_path(sha, path) key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" + cache.fetch(key) do - last_commit_id_for_path(sha, path) + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) + Gitlab::Popen.popen(args, path_to_repo).first.strip end end diff --git a/lib/api/files.rb b/lib/api/files.rb index f95b9be1722..779f54c1ded 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -70,7 +70,7 @@ module API ref: params[:ref], blob_id: blob.id, commit_id: commit.id, - last_commit_id: repo.cache_last_commit_id_for_path(commit.sha, params[:file_path]) + last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) } end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9c66bbdb5f3..af7e89eae05 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -142,16 +142,16 @@ describe Repository, models: true do describe '#last_commit_id_for_path' do subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } - it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } - end + it "returns last commit id for a given path" do + is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + end - describe '#cache_last_commit_id_for_path' do - it "caches #last_commit_id_for_path" do + it "caches last commit id for a given path" do cache = repository.send(:cache) key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') - expect(repository.cache_last_commit_id_for_path(sample_commit.id, '.gitignore')).to eq('c1acaa5') + is_expected.to eq('c1acaa5') end end From ccfbbf7dfac61557159d743b776f145fe380527a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 20 Dec 2016 13:53:24 +0000 Subject: [PATCH 040/206] Fix tests --- .../services/mattermost_slash_command_spec.rb | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 4c08d1e6e65..521eedeae9e 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -32,49 +32,19 @@ feature 'Setup Mattermost slash commands', feature: true do end describe 'mattermost service is enabled' do - let(:info) { find('.services-installation-info') } - before do - Gitlab.config.mattermost.enabled = true + allow(Gitlab.config.mattermost).to receive(:enabled).and_return(true) end - it 'shows the correct mattermost url' do - expect(page).to have_content Gitlab.config.mattermost.host - end - - describe 'mattermost service is active' do - before do - service.active = true - end - - it 'shows that mattermost is active' do - expect(info).to have_content 'Installed' - expect(info).not_to have_content 'Not installed' - end - - it 'shows the edit mattermost button' do - expect(info).to have_button 'Edit Mattermost' - end - end - - describe 'mattermost service is not active' do - before do - service.active = false - end - - it 'shows that mattermost is not active' do - expect(info).to have_content 'Not installed' - end - - it 'shows the add to mattermost button' do - expect(info).to have_button 'Add to Mattermost' - end + it 'shows the add to mattermost button' do + expect(page).to have_link 'Add to Mattermost' end end + describe 'mattermost service is not enabled' do before do - Gitlab.config.mattermost.enabled = false + allow(Gitlab.config.mattermost).to receive(:enabled).and_return(false) end it 'shows the correct trigger url' do From 8d3ed21f2389a3a68dac56c077bc85591bed8b0b Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 20 Dec 2016 14:54:23 +0000 Subject: [PATCH 041/206] Pedro copy changes --- app/views/projects/mattermosts/_team_selection.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 376592e66c9..7980f7c9a72 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -4,7 +4,9 @@ %hr = form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| %h4 Team - %p Select or create the team where the slash commands will be used in + %p + = @teams.one? ? 'The team' : 'Select the team' + where the slash commands will be used in - selected_id = @teams.keys.first if @teams.one? = f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? }) .help-block @@ -22,8 +24,9 @@ %p Choose the word that will trigger commands = f.text_field(:trigger, value: @project.path, class: 'form-control') .help-block - %p Trigger word must be unique, and cannot begin with a slash or contain any spaces. Use the word that works best for your team. - %p Fill in the word that works best for your team. + %p + Trigger word must be unique, and can't begin with a slash or contain any spaces. + Use the word that works best for your team. %p Suggestions: %code= 'gitlab' From b82fdf6257255b720526ccef716759892e88de09 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 20 Dec 2016 17:52:27 +0100 Subject: [PATCH 042/206] Fix error 500 renaming group. Also added specs and changelog. --- app/controllers/groups_controller.rb | 5 +- app/models/namespace.rb | 4 +- app/services/groups/update_service.rb | 8 ++- .../fix-group-path-rename-error.yml | 4 ++ lib/gitlab/update_path_error.rb | 3 ++ spec/controllers/groups_controller_spec.rb | 21 ++++++++ spec/services/groups/update_service_spec.rb | 49 ++++++++++++++++--- 7 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/fix-group-path-rename-error.yml create mode 100644 lib/gitlab/update_path_error.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf..5c7709ea013 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,7 +82,10 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - render action: "edit" + error = group.errors.full_messages.first + alert_message = "Group '#{@group.name}' cannot be updated: " + error + + redirect_to edit_group_path(@group.reload), alert: alert_message end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index fd42f2328d8..b52f08c7081 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base def move_dir if any_project_has_container_registry_tags? - raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index fff2273f402..4e878ec556a 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -14,7 +14,13 @@ module Groups group.assign_attributes(params) - group.save + begin + group.save + rescue Gitlab::UpdatePathError => e + group.errors.add(:base, e.message) + + false + end end end end diff --git a/changelogs/unreleased/fix-group-path-rename-error.yml b/changelogs/unreleased/fix-group-path-rename-error.yml new file mode 100644 index 00000000000..e3d97ae3987 --- /dev/null +++ b/changelogs/unreleased/fix-group-path-rename-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error renaming group +merge_request: +author: diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb new file mode 100644 index 00000000000..ce14cc887d0 --- /dev/null +++ b/lib/gitlab/update_path_error.rb @@ -0,0 +1,3 @@ +module Gitlab + class UpdatePathError < StandardError; end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a763e2c5ba8..4bb37bc52ee 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -105,4 +105,25 @@ describe GroupsController do end end end + + describe 'PUT update' do + before do + sign_in(user) + end + + it 'updates the path succesfully' do + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(response).to have_http_status(302) + expect(controller).to set_flash[:notice] + end + + it 'does not update the path on error' do + allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(response).to have_http_status(302) + expect(controller).to set_flash[:alert] + end + end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 9c2331144a0..8ac5736cbb3 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Groups::UpdateService, services: true do - let!(:user) { create(:user) } - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } - let!(:public_group) { create(:group, :public) } + let!(:user) { create(:user) } + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } + let!(:public_group) { create(:group, :public) } describe "#execute" do context "project visibility_level validation" do context "public group with public projects" do - let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } + let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do public_group.add_user(user, Gitlab::Access::MASTER) @@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do end context "internal group with internal project" do - let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do internal_group.add_user(user, Gitlab::Access::MASTER) @@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do end context "unauthorized visibility_level validation" do - let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } before do internal_group.add_user(user, Gitlab::Access::MASTER) end @@ -49,4 +49,39 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.count).to eq(1) end end + + context 'rename group' do + let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + + before do + internal_group.add_user(user, Gitlab::Access::MASTER) + create(:project, :internal, group: internal_group) + end + + it 'returns true' do + puts internal_group.errors.full_messages + + expect(service.execute).to eq(true) + end + + context 'error moving group' do + before do + allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + end + + it 'does not raise an error' do + expect { service.execute }.not_to raise_error + end + + it 'returns false' do + expect(service.execute).to eq(false) + end + + it 'has the right error' do + service.execute + + expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') + end + end + end end From 08bc8ad9874d646c0d7eb7dca1951b78f7e5df70 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Tue, 20 Dec 2016 17:20:41 +0000 Subject: [PATCH 043/206] Fix copy in Issue Tracker empty state. --- app/views/shared/empty_states/_issues.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index e939278bc07..47379cfb4f8 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -10,9 +10,9 @@ .text-content - if has_button %h4 - The Issue Tracker is a good place to add things that need to be improved or solved in a project! + The Issue Tracker is the place to add things that need to be improved or solved in a project %p - An issue can be a bug, a todo or a feature request that needs to be discussed in a project. + Issues can be bugs, tasks or ideas to be discussed. Besides, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' From 8742c109503c4ad56c99481e22d9fe90e504a371 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Tue, 20 Dec 2016 17:38:06 +0000 Subject: [PATCH 044/206] Add changelog for !8202. https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8202 --- changelogs/unreleased/fix-copy-issues-empty-state.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-copy-issues-empty-state.yml diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml new file mode 100644 index 00000000000..a87b7612217 --- /dev/null +++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Improve copy in Issue Tracker empty state +merge_request: 8202 +author: From 61d09a7b15ef9ae2e23359f1afb87b0adbda4dd4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 19:11:53 +0100 Subject: [PATCH 045/206] WIP --- .../projects/mattermosts_controller.rb | 19 ++++++++++--------- app/helpers/application_helper.rb | 4 ---- app/helpers/projects_helper.rb | 8 -------- .../mattermost_slash_commands_service.rb | 8 ++++++-- lib/mattermost/client.rb | 12 ++++++------ lib/mattermost/session.rb | 16 ++++++++-------- spec/lib/mattermost/command_spec.rb | 18 ++++++++---------- spec/lib/mattermost/team_spec.rb | 2 +- 8 files changed, 39 insertions(+), 48 deletions(-) diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index 0f939838306..d87dff2a80e 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -12,13 +12,17 @@ class Projects::MattermostsController < Projects::ApplicationController end def create - @service.configure!(current_user, configure_params) + result, message = @service.configure(current_user, configure_params) - flash[:notice] = 'This service is now configured' - redirect_to edit_namespace_project_service_path(@project.namespace, @project, service) - rescue => e - flash[:alert] = e.message - redirect_to new_namespace_project_mattermost_path(@project.namespace, @project) + if result + flash[:notice] = 'This service is now configured' + redirect_to edit_namespace_project_service_path( + @project.namespace, @project, service) + else + flash[:alert] = message || 'Failed to configure service' + redirect_to new_namespace_project_mattermost_path( + @project.namespace, @project) + end end private @@ -31,9 +35,6 @@ class Projects::MattermostsController < Projects::ApplicationController def teams @teams ||= @service.list_teams(current_user) - rescue => e - @teams = [] - flash[:alert] = e.message end def service diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index adb5eeee3e4..c816b616631 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -294,8 +294,4 @@ module ApplicationHelper def page_class "issue-boards-page" if current_controller?(:boards) end - - def pretty_url(url) - url.gsub(/\A.*?:\/\//, '') - end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bd2dcb08b3e..d2177f683a1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -148,14 +148,6 @@ module ProjectsHelper ).html_safe end - def mattermost_teams_options(teams) - teams_options = teams.map do |id, options| - [options['display_name'] || options['name'], id] - end - - teams_options.compact.unshift(['Select team...', '0']) - end - private def repo_children_classes(field) diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 572a02b01d4..4c2b7d64f1f 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -19,15 +19,19 @@ class MattermostSlashCommandsService < ChatSlashCommandsService 'mattermost_slash_commands' end - def configure!(user, params) + def configure(user, params) token = Mattermost::Command.new(user). create(command(params)) - update!(active: true, token: token) + update(active: true, token: token) if token + rescue => Mattermost::Error => e + false, e.message end def list_teams(user) Mattermost::Team.new(user).all + rescue => Mattermost::Error => e + [] end private diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index d6759eac0d0..6eaaed34063 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -29,13 +29,13 @@ module Mattermost def json_response(response) json_response = JSON.parse(response.body) - if response.success? - json_response - elsif json_response['message'] - raise ClientError(json_response['message']) - else - raise ClientError('Undefined error') + unless response.success? + raise ClientError(json_response['message'] || 'Undefined error') end + + json_response + rescue JSON::JSONError => e + raise ClientError('Cannot parse response') end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index e36500d24a3..7a7912d02fc 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -3,15 +3,11 @@ module Mattermost class NoSessionError < Error def message - 'No session could be set up, is Mattermost configured with Single Sign on?' + 'No session could be set up, is Mattermost configured with Single Sign On?' end end - class ConnectionError < Error - def message - 'Could not connect. Is Mattermost up?' - end - end + class ConnectionError < Error; end # This class' prime objective is to obtain a session token on a Mattermost # instance with SSO configured where this GitLab instance is the provider. @@ -74,12 +70,16 @@ module Mattermost def get(path, options = {}) self.class.get(path, options.merge(headers: @headers)) - rescue Errno::ECONNREFUSED - raise ConnectionError + rescue HTTParty::Error => e + raise ConnectionError(e.message) + rescue Errno::ECONNREFUSED => e + raise ConnectionError(e.message) end def post(path, options = {}) self.class.post(path, options.merge(headers: @headers)) + rescue HTTParty::Error => e + raise ConnectionError(e.message) rescue Errno::ECONNREFUSED raise ConnectionError end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index f38bb273e7d..ecf336e3199 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -1,19 +1,17 @@ require 'spec_helper' describe Mattermost::Command do - let(:session) { double("session") } let(:hash) { { 'token' => 'token' } } + let(:user) { create(:user) } - describe '.create' do - before do - allow(session).to receive(:post).and_return(hash) - allow(hash).to receive(:parsed_response).and_return(hash) - end + before do + Mattermost::Session.base_uri("http://mattermost.example.com") + end - it 'gets the teams' do - expect(session).to receive(:post) - - described_class.create(session, 'abc', url: 'http://trigger.com') + describe '#create' do + it 'creates a command' do + described_class.new(user). + create(team_id: 'abc', url: 'http://trigger.com') end end end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index c208be3912b..5d1fc6fc603 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Mattermost::Team do - describe '.team_admin' do + describe '#all' do let(:session) { double("session") } let(:response) do From 2b7d759afdb3ce0327367cb7e719afb288334d39 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 20:01:48 +0100 Subject: [PATCH 046/206] Add with_lease to session [ci skip] --- lib/mattermost/session.rb | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 7a7912d02fc..38a71061097 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -26,6 +26,8 @@ module Mattermost include Doorkeeper::Helpers::Controller include HTTParty + LEASE_TIMEOUT = 60 + base_uri Settings.mattermost.host attr_accessor :current_resource_owner, :token @@ -35,14 +37,16 @@ module Mattermost end def with_session - raise NoSessionError unless create + with_lease do + raise NoSessionError unless create - begin - yield self - rescue Errno::ECONNREFUSED - raise NoSessionError - ensure - destroy + begin + yield self + rescue Errno::ECONNREFUSED + raise NoSessionError + ensure + destroy + end end end @@ -130,5 +134,25 @@ module Mattermost response.headers['token'] end end + + def with_lease + lease_uuid = lease_try_obtain + raise NoSessionError unless lease_uuid + + begin + yield + ensure + Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) + end + end + + def lease_key + "mattermost:session" + end + + def lease_try_obtain + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + lease.try_obtain + end end end From dec1e90e505d9ab9e8b088b6a348f5bec293fed1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 20:07:05 +0100 Subject: [PATCH 047/206] Add missing Mattermost::Error --- lib/mattermost/error.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lib/mattermost/error.rb diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 00000000000..014df175be0 --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end From 2393d30da232db04faa8da9e1a958cec22ffb6e8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 20 Dec 2016 20:11:02 +0100 Subject: [PATCH 048/206] Fix rubocop errors [ci skip] --- .../project_services/mattermost_slash_commands_service.rb | 6 +++--- lib/mattermost/client.rb | 2 +- .../projects/services/mattermost_slash_command_spec.rb | 1 - .../mattermost_slash_commands_service_spec.rb | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 4c2b7d64f1f..fc1e7d79d08 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -24,13 +24,13 @@ class MattermostSlashCommandsService < ChatSlashCommandsService create(command(params)) update(active: true, token: token) if token - rescue => Mattermost::Error => e - false, e.message + rescue Mattermost::Error => e + [false, e.message] end def list_teams(user) Mattermost::Team.new(user).all - rescue => Mattermost::Error => e + rescue Mattermost::Error [] end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index 6eaaed34063..fa3c9fa27bd 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -34,7 +34,7 @@ module Mattermost end json_response - rescue JSON::JSONError => e + rescue JSON::JSONError raise ClientError('Cannot parse response') end end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 521eedeae9e..73ecf86aded 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -41,7 +41,6 @@ feature 'Setup Mattermost slash commands', feature: true do end end - describe 'mattermost service is not enabled' do before do allow(Gitlab.config.mattermost).to receive(:enabled).and_return(false) diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 9fb6132d171..07662d2d89a 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -10,13 +10,13 @@ describe MattermostSlashCommandsService, :models do before do allow_any_instance_of(Mattermost::Session).to - receive(:with_session).and_yield + receive(:with_session).and_yield end subject do service.configure!(user, team_id: 'abc', - trigger: 'gitlab', url: 'http://trigger.url', - icon_url: 'http://icon.url/icon.png') + trigger: 'gitlab', url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png') end context 'the requests succeeds' do From 4a03fae930a58b485ee610ce9e6058a2c2dd3d6e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 20 Dec 2016 20:56:46 +0100 Subject: [PATCH 049/206] Fix rspec tests due to different API [ci skip] --- lib/mattermost/session.rb | 4 ++-- .../services/mattermost_slash_command_spec.rb | 12 ++++------- spec/lib/mattermost/command_spec.rb | 15 ++++++++----- spec/lib/mattermost/team_spec.rb | 17 +++++---------- .../mattermost_slash_commands_service_spec.rb | 21 +++++++++++-------- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 38a71061097..8f14ed306b8 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -75,9 +75,9 @@ module Mattermost def get(path, options = {}) self.class.get(path, options.merge(headers: @headers)) rescue HTTParty::Error => e - raise ConnectionError(e.message) + raise Mattermost::ConnectionError.new(e.message) rescue Errno::ECONNREFUSED => e - raise ConnectionError(e.message) + raise Mattermost::ConnectionError.new(e.message) end def post(path, options = {}) diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 73ecf86aded..274d50e7ce4 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -4,10 +4,12 @@ feature 'Setup Mattermost slash commands', feature: true do include WaitForAjax let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } + let(:mattermost_enabled) { true } before do + Settings.mattermost['enabled'] = mattermost_enabled project.team << [user, :master] login_as(user) visit edit_namespace_project_service_path(project.namespace, project, service) @@ -32,19 +34,13 @@ feature 'Setup Mattermost slash commands', feature: true do end describe 'mattermost service is enabled' do - before do - allow(Gitlab.config.mattermost).to receive(:enabled).and_return(true) - end - it 'shows the add to mattermost button' do expect(page).to have_link 'Add to Mattermost' end end describe 'mattermost service is not enabled' do - before do - allow(Gitlab.config.mattermost).to receive(:enabled).and_return(false) - end + let(:mattermost_enabled) { false } it 'shows the correct trigger url' do value = find_field('request_url').value diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index ecf336e3199..f70aee7f3e5 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -1,17 +1,22 @@ require 'spec_helper' describe Mattermost::Command do - let(:hash) { { 'token' => 'token' } } - let(:user) { create(:user) } + let(:params) { { 'token' => 'token', team_id: 'abc' } } + let(:user) { build(:user) } before do Mattermost::Session.base_uri("http://mattermost.example.com") end + subject { described_class.new(user) } + describe '#create' do - it 'creates a command' do - described_class.new(user). - create(team_id: 'abc', url: 'http://trigger.com') + it 'interpolates the team id' do + allow(subject).to receive(:json_post). + with('/api/v3/teams/abc/commands/create', body: params.to_json). + and_return('token' => 'token') + + subject.create(params) end end end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 5d1fc6fc603..ef39c456d5f 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' describe Mattermost::Team do describe '#all' do - let(:session) { double("session") } + let(:user) { build(:user) } + + subject { described_class.new(user) } let(:response) do [{ @@ -20,22 +22,13 @@ describe Mattermost::Team do "allow_open_invite" => false }] end - let(:json) { nil } before do - allow(session).to receive(:get).with('/api/v3/teams/all'). - and_return(json) - allow(json).to receive(:parsed_response).and_return(response) + allow(subject).to receive(:json_get).and_return(response) end it 'gets the teams' do - expect(described_class.all(session).count).to be(1) - end - - it 'filters on being team admin' do - ids = described_class.all(session).map { |team| team['id'] } - - expect(ids).to include("xiyro8huptfhdndadpz8r3wnbo") + expect(subject.all.count).to be(1) end end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 07662d2d89a..850ca45ddd8 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -3,23 +3,23 @@ require 'spec_helper' describe MattermostSlashCommandsService, :models do it_behaves_like "chat slash commands service" - describe '#configure!' do + describe '#configure' do let(:project) { create(:empty_project) } let(:service) { project.build_mattermost_slash_commands_service } let(:user) { create(:user)} - before do - allow_any_instance_of(Mattermost::Session).to - receive(:with_session).and_yield - end - subject do - service.configure!(user, team_id: 'abc', + service.configure(user, team_id: 'abc', trigger: 'gitlab', url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png') end context 'the requests succeeds' do + before do + allow_any_instance_of(Mattermost::Command). + to receive(:json_post).and_return('token' => 'token') + end + it 'saves the service' do expect { subject }.to change { project.services.count }.by(1) end @@ -27,13 +27,16 @@ describe MattermostSlashCommandsService, :models do it 'saves the token' do subject - expect(service.reload.token).to eq('mynewtoken') + expect(service.reload.token).to eq('token') end end context 'an error is received' do it 'shows error messages' do - expect(subject).to raise_error("Error") + succeeded, message = subject + + expect(succeeded).to be(false) + expect(message).to start_with("Failed to open TCP connection to") end end end From 36eed726dcf7e4bf062265cb665ad798be774f89 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 20 Dec 2016 22:30:06 +0100 Subject: [PATCH 050/206] Add controller test --- .../projects/mattermosts_controller_spec.rb | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 spec/controllers/projects/mattermosts_controller_spec.rb diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb new file mode 100644 index 00000000000..3f9482b0cde --- /dev/null +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Projects::MattermostsController do + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET #new' do + before do + get(:new, + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + + it 'accepts the request' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } } + + subject do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + mattermost: mattermost_params) + end + + context 'no request can be made to mattermost' do + it 'shows the error' do + expect(controller).to set_flash[:alert].to /\AFailed to open TCP connection to/ + end + end + + context 'the request is succesull' do + before do + allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token') + end + + it 'redirects to the new page' do + subject + service = project.services.last + + expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service)) + end + end + end +end From 70dcd45de207992b77b23175d7ed9e4643342f72 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 23:08:55 +0100 Subject: [PATCH 051/206] Use separate file for error.rb --- lib/mattermost/session.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 8f14ed306b8..8fcd4a84af5 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,6 +1,4 @@ module Mattermost - class Error < StandardError; end - class NoSessionError < Error def message 'No session could be set up, is Mattermost configured with Single Sign On?' From 22a0567823e792bd760ced79539a0b2c4bfd8f5e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 23:17:32 +0100 Subject: [PATCH 052/206] Fix a few error messages --- app/helpers/mattermost_helper.rb | 9 +++++++++ copy.sh | 11 ----------- lib/mattermost/client.rb | 4 ++-- lib/mattermost/session.rb | 4 ++-- spec/lib/mattermost/team_spec.rb | 4 +--- 5 files changed, 14 insertions(+), 18 deletions(-) create mode 100644 app/helpers/mattermost_helper.rb delete mode 100755 copy.sh diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb new file mode 100644 index 00000000000..49ac12db832 --- /dev/null +++ b/app/helpers/mattermost_helper.rb @@ -0,0 +1,9 @@ +module MattermostHelper + def mattermost_teams_options(teams) + teams_options = teams.map do |id, options| + [options['display_name'] || options['name'], id] + end + + teams_options.compact.unshift(['Select team...', '0']) + end +end diff --git a/copy.sh b/copy.sh deleted file mode 100755 index 2cdc593ef6d..00000000000 --- a/copy.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -xe - -# rsync --delete -av config/{routes.rb,routes,initializers,application.rb} zj-gitlab:/opt/gitlab/embedded/service/gitlab-rails/config/ -rsync --delete -av lib/mattermost zj-gitlab:/opt/gitlab/embedded/service/gitlab-rails/lib -# rsync --delete -av vendor/{assets,gitignore,gitlab-ci-yml} zj-gitlab:/opt/gitlab/embedded/service/gitlab-rails/vendor/ -# rsync --delete -av ../gitlab-shell/{bin,lib,spec,hooks} zj-gitlab:/opt/gitlab/embedded/service/gitlab-shell -#ssh gitlab-test 'cd /opt/gitlab/embedded/service/gitlab-rails && /opt/gitlab/embedded/bin/bundle install --deployment' -#ssh gitlab-test 'export NO_PRIVILEGE_DROP=true; export USE_DB=false; gitlab-rake assets:precompile' -ssh zj-gitlab gitlab-ctl restart diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index fa3c9fa27bd..ec2903b7ec6 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -30,12 +30,12 @@ module Mattermost json_response = JSON.parse(response.body) unless response.success? - raise ClientError(json_response['message'] || 'Undefined error') + raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') end json_response rescue JSON::JSONError - raise ClientError('Cannot parse response') + raise Mattermost::ClientError.new('Cannot parse response') end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 8fcd4a84af5..ddfeb88a71f 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -81,9 +81,9 @@ module Mattermost def post(path, options = {}) self.class.post(path, options.merge(headers: @headers)) rescue HTTParty::Error => e - raise ConnectionError(e.message) + raise Mattermost::ConnectionError.new(e.message) rescue Errno::ECONNREFUSED - raise ConnectionError + raise Mattermost::ConnectionError.new(e.message) end private diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index ef39c456d5f..704579f0f48 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -3,9 +3,6 @@ require 'spec_helper' describe Mattermost::Team do describe '#all' do let(:user) { build(:user) } - - subject { described_class.new(user) } - let(:response) do [{ "id" => "xiyro8huptfhdndadpz8r3wnbo", @@ -22,6 +19,7 @@ describe Mattermost::Team do "allow_open_invite" => false }] end + subject { described_class.new(user) } before do allow(subject).to receive(:json_get).and_return(response) From e9587d5038cbaeca718d6dc08acf906780c0eed0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 20 Dec 2016 17:21:02 -0600 Subject: [PATCH 053/206] apply margin on alert banners only when there is one or more alerts --- app/assets/stylesheets/framework/layout.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 59fae61a44f..5365b62e456 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -33,10 +33,12 @@ body { } .alert-wrapper { - margin-bottom: $gl-padding; - .alert { margin-bottom: 0; + + &:last-child { + margin-bottom: $gl-padding; + } } /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ From 071745141d86385aaa4215be1a5d829618962bb0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 2 Dec 2016 16:44:49 +0530 Subject: [PATCH 054/206] Enable build log autoscroll when page scrolls to bottom --- app/assets/javascripts/build.js | 46 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 824febe3fd3..bb135bec82e 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -10,6 +10,26 @@ Build.state = null; + function isInViewPort(el) { + var elTop = el.offset().top; + var elBottom = elTop + el.outerHeight(); + var vpBottom = $(window).scrollTop() + $(window).height(); + + return vpBottom > elTop; + } + + function toggleAutoScroll(state) { + var $autoScrollBtn = $('#autoscroll-button'); + + if (state) { + $autoScrollBtn.data("state", "enabled"); + $autoScrollBtn.text("Disable autoscroll"); + } else { + $autoScrollBtn.data("state", "disabled"); + $autoScrollBtn.text("Enable autoscroll"); + } + } + function Build(options) { options = options || $('.js-build-options').data(); this.pageUrl = options.pageUrl; @@ -40,18 +60,26 @@ this.initScrollButtonAffix(); } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Bind autoscroll button to follow build output - $('#autoscroll-button').on('click', function() { - var state; - state = $(this).data("state"); - if ("enabled" === state) { - $(this).data("state", "disabled"); - return $(this).text("Enable autoscroll"); + // Bind document scroll listener to detect if user has scrolled to page bottom + // and enable autoscroll of build log, disable autoscroll otherwise + this.$document.on('scroll', function() { + var $autoScrollBtn = $('#autoscroll-button'); + if (isInViewPort($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport + if ($autoScrollBtn.data("state") === 'disabled') { + toggleAutoScroll(true); // Enable Autoscroll + } } else { - $(this).data("state", "enabled"); - return $(this).text("Disable autoscroll"); + if ($autoScrollBtn.data("state") === 'enabled') { + toggleAutoScroll(false); // Disable Autoscroll + } } }); + + // Bind autoscroll button to follow build output + $('#autoscroll-button').on('click', function() { + toggleAutoScroll($(this).data("state") === 'disabled'); + }); + Build.interval = setInterval((function(_this) { // Check for new build output if user still watching build page // Only valid for runnig build when output changes during time From f1f45d4a6f8942040d9741604f503538e991f70a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:02:45 +0530 Subject: [PATCH 055/206] Improve isInViewport impl, autoscroll behavior - Improve isInViewport impementation to be robust. - Autoscroll is now a status indicator instead of toggle button - Scrolling build log enables autoscroll itself when scrolled to bottom. --- app/assets/javascripts/build.js | 128 ++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 46 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index bb135bec82e..99cd5c88479 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -10,24 +10,16 @@ Build.state = null; - function isInViewPort(el) { - var elTop = el.offset().top; - var elBottom = elTop + el.outerHeight(); - var vpBottom = $(window).scrollTop() + $(window).height(); + function isInViewport(el) { + // Courtesy http://stackoverflow.com/a/7557433/414749 + var rect = el[0].getBoundingClientRect(); - return vpBottom > elTop; - } - - function toggleAutoScroll(state) { - var $autoScrollBtn = $('#autoscroll-button'); - - if (state) { - $autoScrollBtn.data("state", "enabled"); - $autoScrollBtn.text("Disable autoscroll"); - } else { - $autoScrollBtn.data("state", "disabled"); - $autoScrollBtn.text("Enable autoscroll"); - } + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= $(window).height() && + rect.right <= $(window).width() + ); } function Build(options) { @@ -52,6 +44,7 @@ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.on('scroll', this.initScrollMonitor.bind(this)); $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); this.updateArtifactRemoveDate(); @@ -60,26 +53,6 @@ this.initScrollButtonAffix(); } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Bind document scroll listener to detect if user has scrolled to page bottom - // and enable autoscroll of build log, disable autoscroll otherwise - this.$document.on('scroll', function() { - var $autoScrollBtn = $('#autoscroll-button'); - if (isInViewPort($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport - if ($autoScrollBtn.data("state") === 'disabled') { - toggleAutoScroll(true); // Enable Autoscroll - } - } else { - if ($autoScrollBtn.data("state") === 'enabled') { - toggleAutoScroll(false); // Disable Autoscroll - } - } - }); - - // Bind autoscroll button to follow build output - $('#autoscroll-button').on('click', function() { - toggleAutoScroll($(this).data("state") === 'disabled'); - }); - Build.interval = setInterval((function(_this) { // Check for new build output if user still watching build page // Only valid for runnig build when output changes during time @@ -150,22 +123,85 @@ }; Build.prototype.checkAutoscroll = function() { - if ("enabled" === $("#autoscroll-button").data("state")) { + if ("enabled" === $("#autoscroll-status").data("state")) { return $("html,body").scrollTop($("#build-trace").height()); } }; Build.prototype.initScrollButtonAffix = function() { - var $body, $buildTrace; - $body = $('body'); - $buildTrace = $('#build-trace'); - return this.$buildScroll.affix({ - offset: { - bottom: function() { - return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); + var $body = $('body'); + var $buildTrace = $('#build-trace'); + var $scrollTopBtn = $('#scroll-top'); + var $scrollBottomBtn = $('#scroll-bottom'); + var $autoScrollContainer = $('.autoscroll-container'); + + $scrollTopBtn.hide().removeClass('sticky'); + $scrollBottomBtn.show().addClass('sticky'); + + if ($autoScrollContainer.length) { + $scrollBottomBtn.hide(); + $autoScrollContainer.show().css({ bottom: 50 }); + } + } + + // Page scroll listener to detect if user has scrolling page + // and handle following cases + // 1) User is at Top of Build Log; + // - Hide Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + // 2) User is at Bottom of Build Log; + // - Show Top Arrow button + // - Hide Bottom Arrow button + // - Enable Autoscroll and show indicator (when build is running) + // 3) User is somewhere in middle of Build Log; + // - Show Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + Build.prototype.initScrollMonitor = function() { + var $body = $('body'); + var $buildTrace = $('#build-trace'); + var $autoScrollContainer = $('.autoscroll-container'); + var $autoScrollStatus = $('#autoscroll-status'); + var $upBuildTrace = $('#up-build-trace'); + var $downBuildTrace = $('#down-build-trace'); + var $scrollTopBtn = $('#scroll-top'); + var $scrollBottomBtn = $('#scroll-bottom'); + + if (isInViewport($upBuildTrace)) { // User is at Top of Build Log + $scrollTopBtn.hide().removeClass('sticky'); + $scrollBottomBtn.show().addClass('sticky'); + } + + if (isInViewport($downBuildTrace)) { // User is at Bottom of Build Log + $scrollTopBtn.show().addClass('sticky'); + $scrollBottomBtn.hide().removeClass('sticky'); + + if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message + $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); + } + } + + if (!isInViewport($upBuildTrace) && !isInViewport($downBuildTrace)) { // User is somewhere in middle of Build Log + $scrollTopBtn.show().addClass('sticky'); + $scrollBottomBtn.show().addClass('sticky'); + + if ($autoScrollContainer.length) { + $autoScrollContainer.hide(); + } + } + + if (this.buildStatus === "running" || this.buildStatus === "pending") { + if (isInViewport($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport + if ($autoScrollStatus.data("state") === 'disabled') { + $autoScrollStatus.data("state", 'enabled'); // Enable Autoscroll + } + } else { + if ($autoScrollStatus.data("state") === 'enabled') { + $autoScrollStatus.data("state", 'disabled'); // Disable Autoscroll } } - }); + } }; Build.prototype.shouldHideSidebarForViewport = function() { From 54fe47b876ebb717616140fb7aa0e506823f6dd1 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:03:28 +0530 Subject: [PATCH 056/206] Enhance styling of scroll buttons, make build log wide --- app/assets/stylesheets/pages/builds.scss | 72 ++++++++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 66f7e7f97c8..76bb09c6c7d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -15,29 +15,69 @@ } .scroll-controls { + height: 100%; + .scroll-step { width: 31px; margin: 0 0 0 auto; } - &.affix-bottom { - position: absolute; + .btn.sticky, + .autoscroll-container { right: 25px; - } - - &.affix { - right: 25px; - bottom: 15px; z-index: 1; } - &.sidebar-expanded { - right: #{$gutter_width + ($gl-padding * 2)}; - } - - a { + .btn { display: block; margin-bottom: 10px; + color: $white-light; + border-color: $white-light; + background-color: transparent; + + &.sticky { + position: fixed; + } + } + + .autoscroll-container { + position: absolute; + + .status-message { + color: $white-light; + + label { + margin-left: 5px; + font-weight: normal; + } + } + } + + #scroll-top.btn.sticky { + top: 115px; + } + + #scroll-bottom.btn.sticky { + bottom: 5px; + } + + &.sidebar-expanded { + + .btn.sticky, + .autoscroll-container { + right: #{$gutter_width + ($gl-padding * 2)}; + } + } + + @media (max-width: $screen-sm-max) { + i.fa { + margin-right: 0; + } + + .scroll-step-label, + .status-message label { + display: none; + } } } @@ -248,6 +288,12 @@ } } +.build-sidebar { + .container-fluid.container-limited { + max-width: 100%; + } +} + .build-detail-row { margin-bottom: 5px; @@ -267,6 +313,8 @@ margin-top: -17px; } + + @media (min-width: $screen-md-min) { .sub-nav.build { width: calc(100% + #{$gutter_width}); From c4dfd07b3615b008feeb177f0aa61d0f83b9f696 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:03:54 +0530 Subject: [PATCH 057/206] Update look of scroll buttons, autoscroll indicator --- app/views/projects/builds/show.html.haml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index cdeb81372ee..70b2140c9ab 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -56,14 +56,18 @@ - else #js-build-scroll.scroll-controls .scroll-step - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down + = link_to '#up-build-trace', id: 'scroll-top', class: 'btn', title: 'Scroll to top' do + %i.fa.fa-long-arrow-up + %span.scroll-step-label Scroll to top + = link_to '#down-build-trace', id: 'scroll-bottom', class: 'btn', title: 'Scroll to bottom' do + %i.fa.fa-long-arrow-down + %span.scroll-step-label Scroll to bottom - if @build.active? .autoscroll-container - %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} - Enable autoscroll + %span.status-message#autoscroll-status{:data => {:state => 'enabled'}} + %i.fa.fa-toggle-on + %label Autoscroll active + #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output = icon("refresh spin", class: "js-build-refresh") From 394d0fd3c8d07538af9d2065178ee4845b25c7d3 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:23:51 +0530 Subject: [PATCH 058/206] ESLint: Clean up nested if blocks --- app/assets/javascripts/build.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 99cd5c88479..e9107f1b553 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -192,15 +192,8 @@ } if (this.buildStatus === "running" || this.buildStatus === "pending") { - if (isInViewport($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport - if ($autoScrollStatus.data("state") === 'disabled') { - $autoScrollStatus.data("state", 'enabled'); // Enable Autoscroll - } - } else { - if ($autoScrollStatus.data("state") === 'enabled') { - $autoScrollStatus.data("state", 'disabled'); // Disable Autoscroll - } - } + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } }; From 3ea8d17e8756fa2fc7978a6ea815ff37ded2e76b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:40:01 +0530 Subject: [PATCH 059/206] ESLint: remove extra indentation --- app/assets/javascripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e9107f1b553..fc4b4aeb73c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -192,8 +192,8 @@ } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } }; From 769a9bcc48d1969f65988f0caae26cd2129e8e3c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 18:03:20 +0530 Subject: [PATCH 060/206] Add changelog entry for MR #7895 --- changelogs/unreleased/19620-auto-scroll-log.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/19620-auto-scroll-log.yml diff --git a/changelogs/unreleased/19620-auto-scroll-log.yml b/changelogs/unreleased/19620-auto-scroll-log.yml new file mode 100644 index 00000000000..eb6e55cbba1 --- /dev/null +++ b/changelogs/unreleased/19620-auto-scroll-log.yml @@ -0,0 +1,4 @@ +--- +title: Improve Build Log scrolling experience +merge_request: 7895 +author: Kushal Pandya From 3d134c50b3baf83b5f675d5e6bda2171924d2ab8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:02:45 +0530 Subject: [PATCH 061/206] Improve isInViewport impl, autoscroll behavior - Improve isInViewport impementation to be robust. - Autoscroll is now a status indicator instead of toggle button - Scrolling build log enables autoscroll itself when scrolled to bottom. --- app/assets/javascripts/build.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index fc4b4aeb73c..874cf0d3023 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -179,6 +179,7 @@ if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); +<<<<<<< fbd5f4507b310d6dc1305696851e9860b48fa0ba } } @@ -195,6 +196,31 @@ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } +======= + } + } + + if (!isInViewport($upBuildTrace) && !isInViewport($downBuildTrace)) { // User is somewhere in middle of Build Log + $scrollTopBtn.show().addClass('sticky'); + $scrollBottomBtn.show().addClass('sticky'); + + if ($autoScrollContainer.length) { + $autoScrollContainer.hide(); + } + } + + if (this.buildStatus === "running" || this.buildStatus === "pending") { + if (isInViewport($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport + if ($autoScrollStatus.data("state") === 'disabled') { + $autoScrollStatus.data("state", 'enabled'); // Enable Autoscroll + } + } else { + if ($autoScrollStatus.data("state") === 'enabled') { + $autoScrollStatus.data("state", 'disabled'); // Disable Autoscroll + } + } + } +>>>>>>> Improve isInViewport impl, autoscroll behavior }; Build.prototype.shouldHideSidebarForViewport = function() { From daa08cbe04028e8303321e16dd96c71bb7a6c703 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:23:51 +0530 Subject: [PATCH 062/206] ESLint: Clean up nested if blocks --- app/assets/javascripts/build.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 874cf0d3023..e0ae5e5ed6a 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -210,15 +210,8 @@ } if (this.buildStatus === "running" || this.buildStatus === "pending") { - if (isInViewport($('.js-build-refresh'))) { // Check if Refresh Animation is in Viewport - if ($autoScrollStatus.data("state") === 'disabled') { - $autoScrollStatus.data("state", 'enabled'); // Enable Autoscroll - } - } else { - if ($autoScrollStatus.data("state") === 'enabled') { - $autoScrollStatus.data("state", 'disabled'); // Disable Autoscroll - } - } + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } >>>>>>> Improve isInViewport impl, autoscroll behavior }; From cf669e5d5a61465aa605b5d31253fe23a5569b00 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:40:01 +0530 Subject: [PATCH 063/206] ESLint: remove extra indentation --- app/assets/javascripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e0ae5e5ed6a..e435ff90075 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -210,8 +210,8 @@ } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } >>>>>>> Improve isInViewport impl, autoscroll behavior }; From 7f6ac4f44c8cf071a6ec2f3f1ffc400dcb5d1e40 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 20:00:34 +0530 Subject: [PATCH 064/206] Add title for autoscroll status indicator for a11y --- app/views/projects/builds/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 70b2140c9ab..3cfbdb2d367 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -64,7 +64,7 @@ %span.scroll-step-label Scroll to bottom - if @build.active? .autoscroll-container - %span.status-message#autoscroll-status{:data => {:state => 'enabled'}} + %span.status-message#autoscroll-status{:data => {:state => 'enabled'}, :title => 'Autoscroll active'} %i.fa.fa-toggle-on %label Autoscroll active #up-build-trace From 985e84a3b24f18f8dd3e15d2f7225124f0c2d5c6 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 15:47:05 +0100 Subject: [PATCH 065/206] added hoverstate on buttons, plus added spacing to make them more square when only the arrows are visible --- app/assets/stylesheets/pages/builds.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 76bb09c6c7d..407d4b1e17a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -38,6 +38,11 @@ &.sticky { position: fixed; } + + &:hover{ + color: $ci-output-bg; + background-color: $white-light; + } } .autoscroll-container { @@ -70,6 +75,10 @@ } @media (max-width: $screen-sm-max) { + .btn { + padding: 6px 13px; + } + i.fa { margin-right: 0; } From 65082b66c3f3baaa088d0ee7bc5b59661960c020 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 15:56:39 +0100 Subject: [PATCH 066/206] added default half transparent background for sticky scroll buttons --- app/assets/stylesheets/pages/builds.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 407d4b1e17a..b8ce74697f1 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -33,7 +33,7 @@ margin-bottom: 10px; color: $white-light; border-color: $white-light; - background-color: transparent; + background-color: rgba($ci-output-bg, .5); &.sticky { position: fixed; From 407c90bebf09fb0668940ba93ddec875a4644116 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 16:01:06 +0100 Subject: [PATCH 067/206] positioned sticky buttons slightly more up and down --- app/assets/stylesheets/pages/builds.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index b8ce74697f1..b256d907649 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -59,11 +59,11 @@ } #scroll-top.btn.sticky { - top: 115px; + top: 110px; } #scroll-bottom.btn.sticky { - bottom: 5px; + bottom: -2px; } &.sidebar-expanded { From 8d7d6bcbd02020ea4b75cb4cec31dfd4b2b6292b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 15:04:42 +0530 Subject: [PATCH 068/206] Fix rebase conflicts --- app/assets/javascripts/build.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e435ff90075..fc4b4aeb73c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -179,7 +179,6 @@ if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); -<<<<<<< fbd5f4507b310d6dc1305696851e9860b48fa0ba } } @@ -196,24 +195,6 @@ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } -======= - } - } - - if (!isInViewport($upBuildTrace) && !isInViewport($downBuildTrace)) { // User is somewhere in middle of Build Log - $scrollTopBtn.show().addClass('sticky'); - $scrollBottomBtn.show().addClass('sticky'); - - if ($autoScrollContainer.length) { - $autoScrollContainer.hide(); - } - } - - if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); - } ->>>>>>> Improve isInViewport impl, autoscroll behavior }; Build.prototype.shouldHideSidebarForViewport = function() { From 1f22e7dc8581378bc75dd01822869c2d08650624 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:24:18 +0530 Subject: [PATCH 069/206] Icon for Build Scroll --- app/assets/images/scroll_down.svg | 1 + app/assets/images/scroll_down_hover_active.svg | 1 + app/assets/images/scroll_up.svg | 1 + app/assets/images/scroll_up_hover_active.svg | 1 + 4 files changed, 4 insertions(+) create mode 100644 app/assets/images/scroll_down.svg create mode 100644 app/assets/images/scroll_down_hover_active.svg create mode 100644 app/assets/images/scroll_up.svg create mode 100644 app/assets/images/scroll_up_hover_active.svg diff --git a/app/assets/images/scroll_down.svg b/app/assets/images/scroll_down.svg new file mode 100644 index 00000000000..5b3a6250a7e --- /dev/null +++ b/app/assets/images/scroll_down.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/scroll_down_hover_active.svg b/app/assets/images/scroll_down_hover_active.svg new file mode 100644 index 00000000000..d622c591df4 --- /dev/null +++ b/app/assets/images/scroll_down_hover_active.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/scroll_up.svg b/app/assets/images/scroll_up.svg new file mode 100644 index 00000000000..0d7a7f51a11 --- /dev/null +++ b/app/assets/images/scroll_up.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/scroll_up_hover_active.svg b/app/assets/images/scroll_up_hover_active.svg new file mode 100644 index 00000000000..86e1cae2c51 --- /dev/null +++ b/app/assets/images/scroll_up_hover_active.svg @@ -0,0 +1 @@ + From 16ad6ed0387f0152ed3a71e643b948be59bcf3d7 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:25:36 +0530 Subject: [PATCH 070/206] Replace scroll buttons with new icons --- app/views/projects/builds/show.html.haml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 3cfbdb2d367..3cbdab07889 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -56,17 +56,13 @@ - else #js-build-scroll.scroll-controls .scroll-step - = link_to '#up-build-trace', id: 'scroll-top', class: 'btn', title: 'Scroll to top' do - %i.fa.fa-long-arrow-up - %span.scroll-step-label Scroll to top - = link_to '#down-build-trace', id: 'scroll-bottom', class: 'btn', title: 'Scroll to bottom' do - %i.fa.fa-long-arrow-down - %span.scroll-step-label Scroll to bottom + %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top'} + %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom'} - if @build.active? .autoscroll-container - %span.status-message#autoscroll-status{:data => {:state => 'enabled'}, :title => 'Autoscroll active'} - %i.fa.fa-toggle-on + %span.status-message#autoscroll-status{:data => {:state => 'enabled'}} %label Autoscroll active + %i.status-icon #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output From ba63e4f1ed9f8d8e452a80c6f96b50c5487510c8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:27:08 +0530 Subject: [PATCH 071/206] Updated styles to use scroll icons --- app/assets/stylesheets/pages/builds.scss | 68 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index b256d907649..0185491bc93 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -22,26 +22,44 @@ margin: 0 0 0 auto; } - .btn.sticky, + .scroll-link.sticky, .autoscroll-container { right: 25px; z-index: 1; } - .btn { + .scroll-link { display: block; margin-bottom: 10px; - color: $white-light; - border-color: $white-light; - background-color: rgba($ci-output-bg, .5); + width: 16px; + height: 33px; + + &.scroll-top { + background-image: image-url('scroll_up'); + + &:hover { + background-image: image-url('scroll_up_hover_active'); + } + } + + &.scroll-bottom { + background-image: image-url('scroll_down'); + + &:hover { + background-image: image-url('scroll_down_hover_active'); + } + } &.sticky { position: fixed; - } - &:hover{ - color: $ci-output-bg; - background-color: $white-light; + &.scroll-top { + top: 110px; + } + + &.scroll-bottom { + bottom: -2px; + } } } @@ -49,33 +67,41 @@ position: absolute; .status-message { + display: inline-block; color: $white-light; + i { + display: inline-block; + width: 16px; + height: 33px; + background-image: image-url('scroll_down_hover_active'); + } + label { - margin-left: 5px; + float: left; + opacity: 0; + margin-right: 10px; font-weight: normal; + line-height: 1.8; + transition: opacity 1s ease-out; + } + + &:hover label { + opacity: 1; } } } - #scroll-top.btn.sticky { - top: 110px; - } - - #scroll-bottom.btn.sticky { - bottom: -2px; - } - &.sidebar-expanded { - .btn.sticky, + .scroll-link.sticky, .autoscroll-container { right: #{$gutter_width + ($gl-padding * 2)}; } } @media (max-width: $screen-sm-max) { - .btn { + .scroll-link { padding: 6px 13px; } @@ -322,8 +348,6 @@ margin-top: -17px; } - - @media (min-width: $screen-md-min) { .sub-nav.build { width: calc(100% + #{$gutter_width}); From c245a667d279066ee819230ec373ee0502c0e0f4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:07:30 +0530 Subject: [PATCH 072/206] Add autoscroll status indicator animation --- app/assets/stylesheets/pages/builds.scss | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 0185491bc93..ece9568cba3 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,3 +1,8 @@ +@keyframes fade-out-status { + 0%, 50% { opacity: 1; } + 100% { opacity: 0; } +} + .build-page { pre.trace { background: $builds-trace-bg; @@ -84,6 +89,10 @@ font-weight: normal; line-height: 1.8; transition: opacity 1s ease-out; + + &.animate { + animation: fade-out-status 2s ease; + } } &:hover label { @@ -99,21 +108,6 @@ right: #{$gutter_width + ($gl-padding * 2)}; } } - - @media (max-width: $screen-sm-max) { - .scroll-link { - padding: 6px 13px; - } - - i.fa { - margin-right: 0; - } - - .scroll-step-label, - .status-message label { - display: none; - } - } } .environment-information { From 1549db64715596437332aaee64eef3f2655a1c77 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:07:59 +0530 Subject: [PATCH 073/206] Add animation to autoscroll indicator when activated --- app/assets/javascripts/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index fc4b4aeb73c..11eaa2858b6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -179,6 +179,7 @@ if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); + $autoScrollStatus.find('label').addClass('animate'); } } @@ -188,6 +189,7 @@ if ($autoScrollContainer.length) { $autoScrollContainer.hide(); + $autoScrollStatus.find('label').removeClass('animate'); } } From 46f0633272e5a9af708bbb09f6c6df9b7734ba0c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:32:43 +0530 Subject: [PATCH 074/206] Autoscroll is disabled by default --- app/views/projects/builds/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 3cbdab07889..bf728983f36 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -60,7 +60,7 @@ %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom'} - if @build.active? .autoscroll-container - %span.status-message#autoscroll-status{:data => {:state => 'enabled'}} + %span.status-message#autoscroll-status{:data => {:state => 'disabled'}} %label Autoscroll active %i.status-icon #up-build-trace From f06678cb1af25a4d7e3b3dae29c15eda58935e59 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:33:00 +0530 Subject: [PATCH 075/206] Fix Autoscroll status icon misalignment on page reload --- app/assets/javascripts/build.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 11eaa2858b6..e14e22c8dae 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -139,8 +139,7 @@ $scrollBottomBtn.show().addClass('sticky'); if ($autoScrollContainer.length) { - $scrollBottomBtn.hide(); - $autoScrollContainer.show().css({ bottom: 50 }); + $autoScrollContainer.hide(); } } From ff0c3fc23b9808438c8959e835937c48297de9b3 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 8 Dec 2016 14:08:08 +0100 Subject: [PATCH 076/206] added a running loading indicator --- app/assets/stylesheets/pages/builds.scss | 33 ++++++++++++++++++++++++ app/views/projects/builds/show.html.haml | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index ece9568cba3..b50971af4e6 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -178,6 +178,39 @@ .bash { display: block; } + + .typing_loader{ + width: 6px; + height: 6px; + border-radius: 50%; + animation: typing 1s linear infinite; + position: relative; + margin-bottom: 12px; + margin-left: 2px; + } + + @keyframes typing{ + 0%{ + background-color: rgba(255,255,255, 1); + box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), + 24px 0px 0px 0px rgba(255,255,255,0.2); + } + 25%{ + background-color: rgba(255,255,255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255,255,255,2), + 24px 0px 0px 0px rgba(255,255,255,0.2); + } + 75%{ + background-color: rgba(255,255,255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), + 24px 0px 0px 0px rgba(255,255,255,1); + } + 100%{ + background-color: rgba(255,255,255, 1); + box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), + 24px 0px 0px 0px rgba(255,255,255,0.2); + } + } } .right-sidebar.build-sidebar { diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index bf728983f36..c32cce15544 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -66,7 +66,7 @@ #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output - = icon("refresh spin", class: "js-build-refresh") + .typing_loader.js-build-refresh #down-build-trace From 2421fc7e089d634e0069285cf2addb0745df53dd Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:06 +0530 Subject: [PATCH 077/206] Refactor SCSS: fix SCSSLints, simplify specificity --- app/assets/stylesheets/pages/builds.scss | 247 ++++++++++++----------- 1 file changed, 125 insertions(+), 122 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index b50971af4e6..23028ae01de 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -3,6 +3,32 @@ 100% { opacity: 0; } } +@keyframes blinking-dots { + 0% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 25% { + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 75%{ + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,1); + } + + 100% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } +} + .build-page { pre.trace { background: $builds-trace-bg; @@ -19,97 +45,6 @@ } } - .scroll-controls { - height: 100%; - - .scroll-step { - width: 31px; - margin: 0 0 0 auto; - } - - .scroll-link.sticky, - .autoscroll-container { - right: 25px; - z-index: 1; - } - - .scroll-link { - display: block; - margin-bottom: 10px; - width: 16px; - height: 33px; - - &.scroll-top { - background-image: image-url('scroll_up'); - - &:hover { - background-image: image-url('scroll_up_hover_active'); - } - } - - &.scroll-bottom { - background-image: image-url('scroll_down'); - - &:hover { - background-image: image-url('scroll_down_hover_active'); - } - } - - &.sticky { - position: fixed; - - &.scroll-top { - top: 110px; - } - - &.scroll-bottom { - bottom: -2px; - } - } - } - - .autoscroll-container { - position: absolute; - - .status-message { - display: inline-block; - color: $white-light; - - i { - display: inline-block; - width: 16px; - height: 33px; - background-image: image-url('scroll_down_hover_active'); - } - - label { - float: left; - opacity: 0; - margin-right: 10px; - font-weight: normal; - line-height: 1.8; - transition: opacity 1s ease-out; - - &.animate { - animation: fade-out-status 2s ease; - } - } - - &:hover label { - opacity: 1; - } - } - } - - &.sidebar-expanded { - - .scroll-link.sticky, - .autoscroll-container { - right: #{$gutter_width + ($gl-padding * 2)}; - } - } - } - .environment-information { background-color: $gray-light; border: 1px solid $border-color; @@ -124,6 +59,97 @@ } } +.scroll-controls { + height: 100%; + + .scroll-step { + width: 31px; + margin: 0 0 0 auto; + } + + .scroll-link.sticky, + .autoscroll-container { + right: 25px; + z-index: 1; + } + + .scroll-link { + display: block; + margin-bottom: 10px; + width: 16px; + height: 33px; + + &.scroll-top { + background-image: image-url('scroll_up'); + + &:hover { + background-image: image-url('scroll_up_hover_active'); + } + } + + &.scroll-bottom { + background-image: image-url('scroll_down'); + + &:hover { + background-image: image-url('scroll_down_hover_active'); + } + } + + &.sticky { + position: fixed; + + &.scroll-top { + top: 110px; + } + + &.scroll-bottom { + bottom: -2px; + } + } + } + + .autoscroll-container { + position: absolute; + } + + &.sidebar-expanded { + + .scroll-link.sticky, + .autoscroll-container { + right: ($gutter_width + ($gl-padding * 2)); + } + } +} + +.status-message { + display: inline-block; + color: $white-light; + + .status-icon { + display: inline-block; + width: 16px; + height: 33px; + background-image: image-url('scroll_down_hover_active'); + } + + .status-text { + float: left; + opacity: 0; + margin-right: 10px; + font-weight: normal; + line-height: 1.8; + transition: opacity 1s ease-out; + + &.animate { + animation: fade-out-status 2s ease; + } + } + + &:hover .status-text { + opacity: 1; + } +} + .build-header { position: relative; padding: 0; @@ -179,37 +205,14 @@ display: block; } - .typing_loader{ - width: 6px; - height: 6px; - border-radius: 50%; - animation: typing 1s linear infinite; - position: relative; - margin-bottom: 12px; - margin-left: 2px; - } - - @keyframes typing{ - 0%{ - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), - 24px 0px 0px 0px rgba(255,255,255,0.2); - } - 25%{ - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,2), - 24px 0px 0px 0px rgba(255,255,255,0.2); - } - 75%{ - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), - 24px 0px 0px 0px rgba(255,255,255,1); - } - 100%{ - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.2), - 24px 0px 0px 0px rgba(255,255,255,0.2); - } + .build-loader-animation { + position: relative; + width: 6px; + height: 6px; + margin-bottom: 12px; + margin-left: 2px; + border-radius: 50%; + animation: blinking-dots 1s linear infinite; } } From 94fd6cf74b44404e5f450a195e252b1b76e844cc Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:41 +0530 Subject: [PATCH 078/206] Update selector for build status text --- app/assets/javascripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e14e22c8dae..51ac8d7970b 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -178,7 +178,7 @@ if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); - $autoScrollStatus.find('label').addClass('animate'); + $autoScrollStatus.find('.status-text').addClass('animate'); } } @@ -188,7 +188,7 @@ if ($autoScrollContainer.length) { $autoScrollContainer.hide(); - $autoScrollStatus.find('label').removeClass('animate'); + $autoScrollStatus.find('.status-text').removeClass('animate'); } } From a8fa87ce9d0dea1baed7c40e10a6b19da99ec6e4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:57 +0530 Subject: [PATCH 079/206] Update class names & element types --- app/views/projects/builds/show.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c32cce15544..0c3b7597c91 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -60,13 +60,13 @@ %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom'} - if @build.active? .autoscroll-container - %span.status-message#autoscroll-status{:data => {:state => 'disabled'}} - %label Autoscroll active + %span.status-message#autoscroll-status{ data: { state: 'disabled' } } + %span.status-text Autoscroll active %i.status-icon #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output - .typing_loader.js-build-refresh + .build-loader-animation.js-build-refresh #down-build-trace From c02945ac95ee3374007249f07211cbf6002a2fa2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:56:25 +0530 Subject: [PATCH 080/206] Cache commonly referenced DOM elements, cleaning up --- app/assets/javascripts/build.js | 69 ++++++++++++++------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 51ac8d7970b..5983bd86e8e 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -31,6 +31,15 @@ this.buildStage = options.buildStage; this.updateDropdown = bind(this.updateDropdown, this); this.$document = $(document); + this.$body = $('body'); + this.$buildTrace = $('#build-trace'); + this.$autoScrollContainer = $('.autoscroll-container'); + this.$autoScrollStatus = $('#autoscroll-status'); + this.$upBuildTrace = $('#up-build-trace'); + this.$downBuildTrace = $('#down-build-trace'); + this.$scrollTopBtn = $('#scroll-top'); + this.$scrollBottomBtn = $('#scroll-bottom'); + clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); @@ -123,24 +132,15 @@ }; Build.prototype.checkAutoscroll = function() { - if ("enabled" === $("#autoscroll-status").data("state")) { + if ($("#autoscroll-status").data("state") === "enabled") { return $("html,body").scrollTop($("#build-trace").height()); } }; Build.prototype.initScrollButtonAffix = function() { - var $body = $('body'); - var $buildTrace = $('#build-trace'); - var $scrollTopBtn = $('#scroll-top'); - var $scrollBottomBtn = $('#scroll-bottom'); - var $autoScrollContainer = $('.autoscroll-container'); - - $scrollTopBtn.hide().removeClass('sticky'); - $scrollBottomBtn.show().addClass('sticky'); - - if ($autoScrollContainer.length) { - $autoScrollContainer.hide(); - } + this.$scrollTopBtn.hide().removeClass('sticky'); + this.$scrollBottomBtn.show().addClass('sticky'); + this.$autoScrollContainer.hide(); } // Page scroll listener to detect if user has scrolling page @@ -158,43 +158,32 @@ // - Show Bottom Arrow button // - Disable Autoscroll and hide indicator (when build is running) Build.prototype.initScrollMonitor = function() { - var $body = $('body'); - var $buildTrace = $('#build-trace'); - var $autoScrollContainer = $('.autoscroll-container'); - var $autoScrollStatus = $('#autoscroll-status'); - var $upBuildTrace = $('#up-build-trace'); - var $downBuildTrace = $('#down-build-trace'); - var $scrollTopBtn = $('#scroll-top'); - var $scrollBottomBtn = $('#scroll-bottom'); - - if (isInViewport($upBuildTrace)) { // User is at Top of Build Log - $scrollTopBtn.hide().removeClass('sticky'); - $scrollBottomBtn.show().addClass('sticky'); + if (isInViewport(this.$upBuildTrace)) { // User is at Top of Build Log + this.$scrollTopBtn.hide().removeClass('sticky'); + this.$scrollBottomBtn.show().addClass('sticky'); } - if (isInViewport($downBuildTrace)) { // User is at Bottom of Build Log - $scrollTopBtn.show().addClass('sticky'); - $scrollBottomBtn.hide().removeClass('sticky'); + if (isInViewport(this.$downBuildTrace)) { // User is at Bottom of Build Log + this.$scrollTopBtn.show().addClass('sticky'); + this.$scrollBottomBtn.hide().removeClass('sticky'); - if ($autoScrollContainer.length) { // Show and Reposition Autoscroll Status Message - $autoScrollContainer.show().css({ top: $body.outerHeight() - 75 }); - $autoScrollStatus.find('.status-text').addClass('animate'); - } + // Show and Reposition Autoscroll Status Indicator + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); + this.$autoScrollStatus.find('.status-text').addClass('animate'); } - if (!isInViewport($upBuildTrace) && !isInViewport($downBuildTrace)) { // User is somewhere in middle of Build Log - $scrollTopBtn.show().addClass('sticky'); - $scrollBottomBtn.show().addClass('sticky'); + if (!isInViewport(this.$upBuildTrace) && !isInViewport(this.$downBuildTrace)) { // User is somewhere in middle of Build Log + this.$scrollTopBtn.show().addClass('sticky'); + this.$scrollBottomBtn.show().addClass('sticky'); - if ($autoScrollContainer.length) { - $autoScrollContainer.hide(); - $autoScrollStatus.find('.status-text').removeClass('animate'); - } + // Hide Autoscroll Status Indicator + this.$autoScrollContainer.hide(); + this.$autoScrollStatus.find('.status-text').removeClass('animate'); } if (this.buildStatus === "running" || this.buildStatus === "pending") { // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - $autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); + this.$autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); } }; From 3202c9c01b6c638ed9a8b7195b83b862b97ee33d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:05:35 +0530 Subject: [PATCH 081/206] Embed SVG using `custom_icon` --- app/assets/images/scroll_down.svg | 1 - app/assets/images/scroll_down_hover_active.svg | 1 - app/assets/images/scroll_up.svg | 1 - app/assets/images/scroll_up_hover_active.svg | 1 - app/views/shared/icons/_scroll_down.svg.erb | 3 +++ app/views/shared/icons/_scroll_down_hover_active.svg.erb | 3 +++ app/views/shared/icons/_scroll_up.svg.erb | 3 +++ app/views/shared/icons/_scroll_up_hover_active.svg.erb | 3 +++ 8 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 app/assets/images/scroll_down.svg delete mode 100644 app/assets/images/scroll_down_hover_active.svg delete mode 100644 app/assets/images/scroll_up.svg delete mode 100644 app/assets/images/scroll_up_hover_active.svg create mode 100644 app/views/shared/icons/_scroll_down.svg.erb create mode 100644 app/views/shared/icons/_scroll_down_hover_active.svg.erb create mode 100644 app/views/shared/icons/_scroll_up.svg.erb create mode 100644 app/views/shared/icons/_scroll_up_hover_active.svg.erb diff --git a/app/assets/images/scroll_down.svg b/app/assets/images/scroll_down.svg deleted file mode 100644 index 5b3a6250a7e..00000000000 --- a/app/assets/images/scroll_down.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/images/scroll_down_hover_active.svg b/app/assets/images/scroll_down_hover_active.svg deleted file mode 100644 index d622c591df4..00000000000 --- a/app/assets/images/scroll_down_hover_active.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/images/scroll_up.svg b/app/assets/images/scroll_up.svg deleted file mode 100644 index 0d7a7f51a11..00000000000 --- a/app/assets/images/scroll_up.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/images/scroll_up_hover_active.svg b/app/assets/images/scroll_up_hover_active.svg deleted file mode 100644 index 86e1cae2c51..00000000000 --- a/app/assets/images/scroll_up_hover_active.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/views/shared/icons/_scroll_down.svg.erb b/app/views/shared/icons/_scroll_down.svg.erb new file mode 100644 index 00000000000..acf22ac9314 --- /dev/null +++ b/app/views/shared/icons/_scroll_down.svg.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg.erb b/app/views/shared/icons/_scroll_down_hover_active.svg.erb new file mode 100644 index 00000000000..262576acf54 --- /dev/null +++ b/app/views/shared/icons/_scroll_down_hover_active.svg.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/icons/_scroll_up.svg.erb b/app/views/shared/icons/_scroll_up.svg.erb new file mode 100644 index 00000000000..f11288fd59c --- /dev/null +++ b/app/views/shared/icons/_scroll_up.svg.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg.erb b/app/views/shared/icons/_scroll_up_hover_active.svg.erb new file mode 100644 index 00000000000..4658dbb1bb7 --- /dev/null +++ b/app/views/shared/icons/_scroll_up_hover_active.svg.erb @@ -0,0 +1,3 @@ + + + From d144160f9f1f7b59ea2cc49d511b6a195b8b6e2d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:06:21 +0530 Subject: [PATCH 082/206] Flip embedded SVG on hover --- app/assets/stylesheets/pages/builds.scss | 27 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 23028ae01de..04cd9db2acf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -80,18 +80,36 @@ height: 33px; &.scroll-top { - background-image: image-url('scroll_up'); + .gitlab-icon-scroll-up-hover { + display: none; + } &:hover { - background-image: image-url('scroll_up_hover_active'); + + .gitlab-icon-scroll-up { + display: none; + } + + .gitlab-icon-scroll-up-hover { + display: inline-block; + } } } &.scroll-bottom { - background-image: image-url('scroll_down'); + .gitlab-icon-scroll-down-hover { + display: none; + } &:hover { - background-image: image-url('scroll_down_hover_active'); + + .gitlab-icon-scroll-down { + display: none; + } + + .gitlab-icon-scroll-down-hover { + display: inline-block; + } } } @@ -129,7 +147,6 @@ display: inline-block; width: 16px; height: 33px; - background-image: image-url('scroll_down_hover_active'); } .status-text { From 5fc161c2a78710bb026e73bd502e4955022ec3b0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:06:38 +0530 Subject: [PATCH 083/206] Embed SVG using `custom_icon` --- app/views/projects/builds/show.html.haml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 0c3b7597c91..c69c53b656f 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -56,13 +56,18 @@ - else #js-build-scroll.scroll-controls .scroll-step - %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top'} - %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom'} + %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top' } + = custom_icon('scroll_up') + = custom_icon('scroll_up_hover_active') + %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom' } + = custom_icon('scroll_down') + = custom_icon('scroll_down_hover_active') - if @build.active? .autoscroll-container %span.status-message#autoscroll-status{ data: { state: 'disabled' } } %span.status-text Autoscroll active %i.status-icon + = custom_icon('scroll_down_hover_active') #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output From df0aeae7da14fa3627c5627c11c521d91170aca9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:05:36 +0530 Subject: [PATCH 084/206] Move `isInViewport` to `gl.utils`, make scroll buttons sticky always --- app/assets/javascripts/build.js | 36 +++++++++++---------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 5983bd86e8e..56b48b2cd60 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -10,18 +10,6 @@ Build.state = null; - function isInViewport(el) { - // Courtesy http://stackoverflow.com/a/7557433/414749 - var rect = el[0].getBoundingClientRect(); - - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= $(window).height() && - rect.right <= $(window).width() - ); - } - function Build(options) { options = options || $('.js-build-options').data(); this.pageUrl = options.pageUrl; @@ -138,8 +126,8 @@ }; Build.prototype.initScrollButtonAffix = function() { - this.$scrollTopBtn.hide().removeClass('sticky'); - this.$scrollBottomBtn.show().addClass('sticky'); + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.show(); this.$autoScrollContainer.hide(); } @@ -158,23 +146,23 @@ // - Show Bottom Arrow button // - Disable Autoscroll and hide indicator (when build is running) Build.prototype.initScrollMonitor = function() { - if (isInViewport(this.$upBuildTrace)) { // User is at Top of Build Log - this.$scrollTopBtn.hide().removeClass('sticky'); - this.$scrollBottomBtn.show().addClass('sticky'); + if (gl.utils.isInViewport(this.$upBuildTrace[0])) { // User is at Top of Build Log + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.show(); } - if (isInViewport(this.$downBuildTrace)) { // User is at Bottom of Build Log - this.$scrollTopBtn.show().addClass('sticky'); - this.$scrollBottomBtn.hide().removeClass('sticky'); + if (gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Bottom of Build Log + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); this.$autoScrollStatus.find('.status-text').addClass('animate'); } - if (!isInViewport(this.$upBuildTrace) && !isInViewport(this.$downBuildTrace)) { // User is somewhere in middle of Build Log - this.$scrollTopBtn.show().addClass('sticky'); - this.$scrollBottomBtn.show().addClass('sticky'); + if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.show(); // Hide Autoscroll Status Indicator this.$autoScrollContainer.hide(); @@ -183,7 +171,7 @@ if (this.buildStatus === "running" || this.buildStatus === "pending") { // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - this.$autoScrollStatus.data("state", isInViewport($('.js-build-refresh')) ? 'enabled' : 'disabled'); + this.$autoScrollStatus.data("state", gl.utils.isInViewport($('.js-build-refresh')[0]) ? 'enabled' : 'disabled'); } }; From 719e59c71066748f10bedab35b4006e37e3d025f Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:06:03 +0530 Subject: [PATCH 085/206] Make scroll buttons sticky always --- app/assets/stylesheets/pages/builds.scss | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 04cd9db2acf..173bc2d80cf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -67,19 +67,20 @@ margin: 0 0 0 auto; } - .scroll-link.sticky, + .scroll-link, .autoscroll-container { right: 25px; z-index: 1; } .scroll-link { + position: fixed; display: block; margin-bottom: 10px; - width: 16px; - height: 33px; &.scroll-top { + top: 110px; + .gitlab-icon-scroll-up-hover { display: none; } @@ -97,6 +98,8 @@ } &.scroll-bottom { + bottom: -2px; + .gitlab-icon-scroll-down-hover { display: none; } @@ -112,18 +115,6 @@ } } } - - &.sticky { - position: fixed; - - &.scroll-top { - top: 110px; - } - - &.scroll-bottom { - bottom: -2px; - } - } } .autoscroll-container { @@ -132,7 +123,7 @@ &.sidebar-expanded { - .scroll-link.sticky, + .scroll-link, .autoscroll-container { right: ($gutter_width + ($gl-padding * 2)); } From 6fe5bb40b6dde14b3594a37b008265e1f9083296 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:06:18 +0530 Subject: [PATCH 086/206] Add `isInViewport` --- app/assets/javascripts/lib/utils/common_utils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8fa80502d92..0a0e73e0ccc 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -93,6 +93,19 @@ } }; + // Check if element scrolled into viewport from above or below + // Courtesy http://stackoverflow.com/a/7557433/414749 + w.gl.utils.isInViewport = function(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); + }; + gl.utils.getPagePath = function() { return $('body').data('page').split(':')[0]; }; From ad0b81872a52e984f240f229c3de03bae4d44c7e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:34:19 +0530 Subject: [PATCH 087/206] Fix incorrect scroll button appearance in certain cases --- app/assets/javascripts/build.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 56b48b2cd60..b4e1d3d9346 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -81,6 +81,7 @@ }; Build.prototype.getInitialBuildTrace = function() { + var _this = this; var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] return $.ajax({ @@ -89,6 +90,7 @@ success: function(buildData) { $('.js-build-output').html(buildData.trace_html); if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { + _this.initScrollMonitor(); return $('.js-build-refresh').remove(); } } @@ -123,11 +125,20 @@ if ($("#autoscroll-status").data("state") === "enabled") { return $("html,body").scrollTop($("#build-trace").height()); } + + // Handle a situation where user started new build + // but never scrolled a page + if (!this.$scrollTopBtn.is(':visible') && + !this.$scrollBottomBtn.is(':visible') && + !gl.utils.isInViewport(this.$downBuildTrace[0])) { + this.$scrollBottomBtn.show(); + } }; Build.prototype.initScrollButtonAffix = function() { + // Hide everything initially this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.show(); + this.$scrollBottomBtn.hide(); this.$autoScrollContainer.hide(); } From 95d535921214740d0f26f65b1567aa03b9e53b2b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 12:17:20 +0530 Subject: [PATCH 088/206] ESLint: use outer `this` by currying function --- app/assets/javascripts/build.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index b4e1d3d9346..5c6f0083e57 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -81,19 +81,20 @@ }; Build.prototype.getInitialBuildTrace = function() { - var _this = this; var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] return $.ajax({ url: this.buildUrl, dataType: 'json', - success: function(buildData) { - $('.js-build-output').html(buildData.trace_html); - if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { - _this.initScrollMonitor(); - return $('.js-build-refresh').remove(); - } - } + success: (function(_this) { + return function(buildData) { + $('.js-build-output').html(buildData.trace_html); + if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { + _this.initScrollMonitor(); + return $('.js-build-refresh').remove(); + } + }; + })(this) }); }; From 2b6b28da740476c02a7bab2113737458e3e6af30 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 12:17:50 +0530 Subject: [PATCH 089/206] SCSSLint: Space before brace --- app/assets/stylesheets/pages/builds.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 173bc2d80cf..2f7448a0b9d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -16,7 +16,7 @@ 24px 0 0 0 rgba($white-light,0.2); } - 75%{ + 75% { background-color: rgba($white-light, 0.4); box-shadow: 12px 0 0 0 rgba($white-light,0.2), 24px 0 0 0 rgba($white-light,1); From 45220e310cd12da9bd7b0b07fe3509def063a60c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 22:05:17 +0530 Subject: [PATCH 090/206] Use `bind` instead of currying to access outer context --- app/assets/javascripts/build.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 5c6f0083e57..8646ce99726 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -86,15 +86,13 @@ return $.ajax({ url: this.buildUrl, dataType: 'json', - success: (function(_this) { - return function(buildData) { - $('.js-build-output').html(buildData.trace_html); - if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { - _this.initScrollMonitor(); - return $('.js-build-refresh').remove(); - } - }; - })(this) + success: function(buildData) { + $('.js-build-output').html(buildData.trace_html); + if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { + this.initScrollMonitor(); + return $('.js-build-refresh').remove(); + } + }.bind(this) }); }; From 3ed1f6dca770f11e9b86eb3163bbd5476c94e4e0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 22:12:24 +0530 Subject: [PATCH 091/206] Cache common references to `.status-text` element --- app/assets/javascripts/build.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 8646ce99726..d8c10457a4a 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -23,6 +23,7 @@ this.$buildTrace = $('#build-trace'); this.$autoScrollContainer = $('.autoscroll-container'); this.$autoScrollStatus = $('#autoscroll-status'); + this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); this.$upBuildTrace = $('#up-build-trace'); this.$downBuildTrace = $('#down-build-trace'); this.$scrollTopBtn = $('#scroll-top'); @@ -167,7 +168,7 @@ // Show and Reposition Autoscroll Status Indicator this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); - this.$autoScrollStatus.find('.status-text').addClass('animate'); + this.$autoScrollStatusText.addClass('animate'); } if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log @@ -176,7 +177,7 @@ // Hide Autoscroll Status Indicator this.$autoScrollContainer.hide(); - this.$autoScrollStatus.find('.status-text').removeClass('animate'); + this.$autoScrollStatusText.removeClass('animate'); } if (this.buildStatus === "running" || this.buildStatus === "pending") { From dfd156972df7d47465e9222e1979fa9c91b702aa Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 18:14:04 +0530 Subject: [PATCH 092/206] Refactor build log scroll button logic to prevent icon overlaps --- app/assets/javascripts/build.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index d8c10457a4a..36612957b39 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -28,6 +28,7 @@ this.$downBuildTrace = $('#down-build-trace'); this.$scrollTopBtn = $('#scroll-top'); this.$scrollBottomBtn = $('#scroll-bottom'); + this.$buildRefreshAnimation = $('.js-build-refresh'); clearInterval(Build.interval); // Init breakpoint checker @@ -91,7 +92,7 @@ $('.js-build-output').html(buildData.trace_html); if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { this.initScrollMonitor(); - return $('.js-build-refresh').remove(); + return this.$buildRefreshAnimation.remove(); } }.bind(this) }); @@ -157,23 +158,27 @@ // - Show Bottom Arrow button // - Disable Autoscroll and hide indicator (when build is running) Build.prototype.initScrollMonitor = function() { - if (gl.utils.isInViewport(this.$upBuildTrace[0])) { // User is at Top of Build Log + if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.show(); + + // Hide Autoscroll Status Indicator + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Top of Build Log this.$scrollTopBtn.hide(); this.$scrollBottomBtn.show(); - } - - if (gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Bottom of Build Log + } else if ((!gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) || + gl.utils.isInViewport(this.$buildRefreshAnimation[0])) { // User is at Bottom of Build Log this.$scrollTopBtn.show(); this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); this.$autoScrollStatusText.addClass('animate'); - } - - if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log - this.$scrollTopBtn.show(); - this.$scrollBottomBtn.show(); + } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) { // Build Log height is small + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); // Hide Autoscroll Status Indicator this.$autoScrollContainer.hide(); @@ -182,7 +187,7 @@ if (this.buildStatus === "running" || this.buildStatus === "pending") { // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - this.$autoScrollStatus.data("state", gl.utils.isInViewport($('.js-build-refresh')[0]) ? 'enabled' : 'disabled'); + this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation[0]) ? 'enabled' : 'disabled'); } }; From cc62a416560267d4a87b823faa333bcdaa5867b4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 21:13:27 +0530 Subject: [PATCH 093/206] Fix case where Autoscroll indicator overlays scroll bottom button --- app/assets/javascripts/build.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 36612957b39..81149a427dc 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -160,11 +160,23 @@ Build.prototype.initScrollMonitor = function() { if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log this.$scrollTopBtn.show(); - this.$scrollBottomBtn.show(); + + if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed + this.$scrollBottomBtn.show(); + } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation[0])) { + this.$scrollBottomBtn.show(); + } else { + this.$scrollBottomBtn.hide(); + } // Hide Autoscroll Status Indicator - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); + if (this.$scrollBottomBtn.is(':visible')) { + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else { + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); + this.$autoScrollStatusText.addClass('animate'); + } } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Top of Build Log this.$scrollTopBtn.hide(); this.$scrollBottomBtn.show(); From 3f2fd48c7cdcbefc4d26b5b45c9150433d394271 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 21:21:13 +0530 Subject: [PATCH 094/206] Prevent scroll bottom button and scroll indicator clashes --- app/assets/javascripts/build.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 81149a427dc..c3cbd07ae21 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -180,6 +180,9 @@ } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Top of Build Log this.$scrollTopBtn.hide(); this.$scrollBottomBtn.show(); + + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); } else if ((!gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) || gl.utils.isInViewport(this.$buildRefreshAnimation[0])) { // User is at Bottom of Build Log this.$scrollTopBtn.show(); From 63ca9fd0c85fbb5364ee8bf96e78e8ea6bb0427b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 14 Dec 2016 21:46:45 +0530 Subject: [PATCH 095/206] Combine common rules --- app/assets/stylesheets/pages/builds.scss | 45 +++++++----------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 2f7448a0b9d..f9e8d297c05 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -78,42 +78,24 @@ display: block; margin-bottom: 10px; + &.scroll-top .gitlab-icon-scroll-up-hover, + &.scroll-top:hover .gitlab-icon-scroll-up, + &.scroll-bottom .gitlab-icon-scroll-down-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down { + display: none; + } + + &.scroll-top:hover .gitlab-icon-scroll-up-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down-hover { + display: inline-block; + } + &.scroll-top { top: 110px; - - .gitlab-icon-scroll-up-hover { - display: none; - } - - &:hover { - - .gitlab-icon-scroll-up { - display: none; - } - - .gitlab-icon-scroll-up-hover { - display: inline-block; - } - } } &.scroll-bottom { bottom: -2px; - - .gitlab-icon-scroll-down-hover { - display: none; - } - - &:hover { - - .gitlab-icon-scroll-down { - display: none; - } - - .gitlab-icon-scroll-down-hover { - display: inline-block; - } - } } } @@ -217,8 +199,7 @@ position: relative; width: 6px; height: 6px; - margin-bottom: 12px; - margin-left: 2px; + margin: auto auto 12px 2px; border-radius: 50%; animation: blinking-dots 1s linear infinite; } From 3817daa37a77193c5f9479136a06c4737b037818 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 14 Dec 2016 21:47:08 +0530 Subject: [PATCH 096/206] Fix scroll top button overlapping --- app/assets/javascripts/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c3cbd07ae21..1cf5e22d7f6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -184,7 +184,7 @@ this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); } else if ((!gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) || - gl.utils.isInViewport(this.$buildRefreshAnimation[0])) { // User is at Bottom of Build Log + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation[0]))) { // User is at Bottom of Build Log this.$scrollTopBtn.show(); this.$scrollBottomBtn.hide(); From dbd21446e5a9550e80ec8f3f3448e75808208e80 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Dec 2016 11:31:09 +0530 Subject: [PATCH 097/206] Remove author name --- changelogs/unreleased/19620-auto-scroll-log.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/19620-auto-scroll-log.yml b/changelogs/unreleased/19620-auto-scroll-log.yml index eb6e55cbba1..cf38096683b 100644 --- a/changelogs/unreleased/19620-auto-scroll-log.yml +++ b/changelogs/unreleased/19620-auto-scroll-log.yml @@ -1,4 +1,4 @@ --- title: Improve Build Log scrolling experience merge_request: 7895 -author: Kushal Pandya +author: From 09738ad09843721d0b0849702daf51292a372df1 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 10:04:12 +0530 Subject: [PATCH 098/206] Use `get(0)` instead of direct array index access --- app/assets/javascripts/build.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 1cf5e22d7f6..867238e205e 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -131,7 +131,7 @@ // but never scrolled a page if (!this.$scrollTopBtn.is(':visible') && !this.$scrollBottomBtn.is(':visible') && - !gl.utils.isInViewport(this.$downBuildTrace[0])) { + !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { this.$scrollBottomBtn.show(); } }; @@ -158,12 +158,12 @@ // - Show Bottom Arrow button // - Disable Autoscroll and hide indicator (when build is running) Build.prototype.initScrollMonitor = function() { - if (!gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is somewhere in middle of Build Log + if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is somewhere in middle of Build Log this.$scrollTopBtn.show(); if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed this.$scrollBottomBtn.show(); - } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation[0])) { + } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { this.$scrollBottomBtn.show(); } else { this.$scrollBottomBtn.hide(); @@ -177,21 +177,21 @@ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); this.$autoScrollStatusText.addClass('animate'); } - } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && !gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Top of Build Log + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is at Top of Build Log this.$scrollTopBtn.hide(); this.$scrollBottomBtn.show(); this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); - } else if ((!gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) || - (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation[0]))) { // User is at Bottom of Build Log + } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { // User is at Bottom of Build Log this.$scrollTopBtn.show(); this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); this.$autoScrollStatusText.addClass('animate'); - } else if (gl.utils.isInViewport(this.$upBuildTrace[0]) && gl.utils.isInViewport(this.$downBuildTrace[0])) { // Build Log height is small + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // Build Log height is small this.$scrollTopBtn.hide(); this.$scrollBottomBtn.hide(); @@ -202,7 +202,7 @@ if (this.buildStatus === "running" || this.buildStatus === "pending") { // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation[0]) ? 'enabled' : 'disabled'); + this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled'); } }; From 5bab7c57e3e108d603ceb41c53091ebfb027a37e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 10:11:05 +0530 Subject: [PATCH 099/206] Set offset via constant variable --- app/assets/javascripts/build.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 867238e205e..eeafe436d6c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -4,6 +4,7 @@ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var AUTO_SCROLL_OFFSET = 75; this.Build = (function() { Build.interval = null; @@ -174,7 +175,7 @@ this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); } else { - this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).fadeIn(100); this.$autoScrollStatusText.addClass('animate'); } } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is at Top of Build Log @@ -189,7 +190,7 @@ this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator - this.$autoScrollContainer.css({ top: this.$body.outerHeight() - 75 }).fadeIn(100); + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).fadeIn(100); this.$autoScrollStatusText.addClass('animate'); } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // Build Log height is small this.$scrollTopBtn.hide(); From 7dfd5aa69ed0db0157b475973ffcea592098267a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:10:35 +0530 Subject: [PATCH 100/206] Use cached element references --- app/assets/javascripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index eeafe436d6c..c9230a2d090 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -124,8 +124,8 @@ }; Build.prototype.checkAutoscroll = function() { - if ($("#autoscroll-status").data("state") === "enabled") { - return $("html,body").scrollTop($("#build-trace").height()); + if (this.$autoScrollStatus.data("state") === "enabled") { + return $("html,body").scrollTop(this.$buildTrace.height()); } // Handle a situation where user started new build From b4aabafaa48043a8b17b3bf927a684174ad31abd Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:31:51 +0530 Subject: [PATCH 101/206] Replace `$.fadeIn()` usage with `$.show()` --- app/assets/javascripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c9230a2d090..d6a9ab685f2 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -175,7 +175,7 @@ this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); } else { - this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).fadeIn(100); + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); this.$autoScrollStatusText.addClass('animate'); } } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is at Top of Build Log @@ -190,7 +190,7 @@ this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator - this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).fadeIn(100); + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); this.$autoScrollStatusText.addClass('animate'); } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // Build Log height is small this.$scrollTopBtn.hide(); From 11ba7a885674dd5428b92c2e43a109fa098320f4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:42:11 +0530 Subject: [PATCH 102/206] Move comments to next line --- app/assets/javascripts/build.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index d6a9ab685f2..5e449170cd3 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -159,7 +159,9 @@ // - Show Bottom Arrow button // - Disable Autoscroll and hide indicator (when build is running) Build.prototype.initScrollMonitor = function() { - if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is somewhere in middle of Build Log + if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is somewhere in middle of Build Log + this.$scrollTopBtn.show(); if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed @@ -178,21 +180,27 @@ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); this.$autoScrollStatusText.addClass('animate'); } - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // User is at Top of Build Log + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is at Top of Build Log + this.$scrollTopBtn.hide(); this.$scrollBottomBtn.show(); this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || - (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { // User is at Bottom of Build Log + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { + // User is at Bottom of Build Log + this.$scrollTopBtn.show(); this.$scrollBottomBtn.hide(); // Show and Reposition Autoscroll Status Indicator this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); this.$autoScrollStatusText.addClass('animate'); - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { // Build Log height is small + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // Build Log height is small + this.$scrollTopBtn.hide(); this.$scrollBottomBtn.hide(); From 120eeb5b5163f91220572de0d50c8359f115b7a8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:58:32 +0530 Subject: [PATCH 103/206] Renamed with `.svg` --- app/views/shared/icons/{_scroll_down.svg.erb => _scroll_down.svg} | 0 ...ll_down_hover_active.svg.erb => _scroll_down_hover_active.svg} | 0 app/views/shared/icons/{_scroll_up.svg.erb => _scroll_up.svg} | 0 ...scroll_up_hover_active.svg.erb => _scroll_up_hover_active.svg} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename app/views/shared/icons/{_scroll_down.svg.erb => _scroll_down.svg} (100%) rename app/views/shared/icons/{_scroll_down_hover_active.svg.erb => _scroll_down_hover_active.svg} (100%) rename app/views/shared/icons/{_scroll_up.svg.erb => _scroll_up.svg} (100%) rename app/views/shared/icons/{_scroll_up_hover_active.svg.erb => _scroll_up_hover_active.svg} (100%) diff --git a/app/views/shared/icons/_scroll_down.svg.erb b/app/views/shared/icons/_scroll_down.svg similarity index 100% rename from app/views/shared/icons/_scroll_down.svg.erb rename to app/views/shared/icons/_scroll_down.svg diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg.erb b/app/views/shared/icons/_scroll_down_hover_active.svg similarity index 100% rename from app/views/shared/icons/_scroll_down_hover_active.svg.erb rename to app/views/shared/icons/_scroll_down_hover_active.svg diff --git a/app/views/shared/icons/_scroll_up.svg.erb b/app/views/shared/icons/_scroll_up.svg similarity index 100% rename from app/views/shared/icons/_scroll_up.svg.erb rename to app/views/shared/icons/_scroll_up.svg diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg.erb b/app/views/shared/icons/_scroll_up_hover_active.svg similarity index 100% rename from app/views/shared/icons/_scroll_up_hover_active.svg.erb rename to app/views/shared/icons/_scroll_up_hover_active.svg From 6e186b76bb22c5803e3d7105f8ff78c11128812d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 11 Dec 2016 14:09:37 +0200 Subject: [PATCH 104/206] Added support for Authentiq oauth provider --- Gemfile | 1 + Gemfile.lock | 3 + .../images/auth_buttons/authentiq_64.png | Bin 0 -> 17679 bytes app/helpers/auth_helper.rb | 2 +- .../8038-authentiq-id-oauth-support.yml | 4 + config/gitlab.yml.example | 10 +++ doc/administration/auth/README.md | 2 +- doc/administration/auth/authentiq.md | 69 ++++++++++++++++++ doc/integration/README.md | 2 +- doc/integration/omniauth.md | 1 + 10 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/auth_buttons/authentiq_64.png create mode 100644 changelogs/unreleased/8038-authentiq-id-oauth-support.yml create mode 100644 doc/administration/auth/authentiq.md diff --git a/Gemfile b/Gemfile index bea31b53b1c..dc28b4fdd9b 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' +gem 'omniauth-authentiq', '~> 0.2.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt' diff --git a/Gemfile.lock b/Gemfile.lock index 811adfc5c1d..e0266d8fdc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -428,6 +428,8 @@ GEM rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) + omniauth-authentiq (0.2.2) + omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -897,6 +899,7 @@ DEPENDENCIES oj (~> 2.17.4) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) + omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png new file mode 100644 index 0000000000000000000000000000000000000000..81767bbcc54f114ec3d6436d3abf640f260dc169 GIT binary patch literal 17679 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 V+z4D23OyPDGu*s*1uh{e{{ "authentiq", + "app_id" => "YOUR_CLIENT_ID", + "app_secret" => "YOUR_CLIENT_SECRET", + "args" => { + scope: 'aq:name email~rs aq:push' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'authentiq', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET', + args: { + scope: 'aq:name email~rs aq:push' + } + } + ``` + + +5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. +See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. + +6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. + +7. Save the configuration file. + +8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) + for the changes to take effect if you installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process. + +- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation. +- If not they will be prompted to download the app and then follow the procedure above. + +If everything goes right, the user will be returned to GitLab and will be signed in. \ No newline at end of file diff --git a/doc/integration/README.md b/doc/integration/README.md index f8ffa6dcb7f..ed843c0bfa9 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services. - [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8a55fce96fe..4c933cef9b7 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -30,6 +30,7 @@ contains some settings that are common for all providers. - [Crowd](crowd.md) - [Azure](azure.md) - [Auth0](auth0.md) +- [Authentiq](../administration/auth/authentiq.md) ## Initial OmniAuth Configuration From 5452747729d30733a51a85ff1212d0724af1d1ff Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 10:33:20 +0100 Subject: [PATCH 105/206] Fix error importing label priorities and added relevant spec --- lib/gitlab/import_export/relation_factory.rb | 28 ++++++++++++++++---- spec/lib/gitlab/import_export/project.json | 22 +++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 65b229ca8ff..7a649f28340 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -22,7 +22,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze def self.create(*args) new(*args).create @@ -189,7 +189,7 @@ module Gitlab # Otherwise always create the record, skipping the extra SELECT clause. @existing_or_new_object ||= begin if EXISTING_OBJECT_CHECK.include?(@relation_name) - attribute_hash = attribute_hash_for(['events', 'priorities']) + attribute_hash = attribute_hash_for(['events']) existing_object.assign_attributes(attribute_hash) if attribute_hash.any? @@ -210,9 +210,8 @@ module Gitlab def existing_object @existing_object ||= begin - finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] - finder_hash = parsed_relation_hash.slice(*finder_attributes) - existing_object = relation_class.find_or_create_by(finder_hash) + existing_object = find_or_create_object! + # Done in two steps, as MySQL behaves differently than PostgreSQL using # the +find_or_create_by+ method and does not return the ID the second time. existing_object.update!(parsed_relation_hash) @@ -224,6 +223,25 @@ module Gitlab @relation_name == :services && parsed_relation_hash['type'] && !Object.const_defined?(parsed_relation_hash['type']) end + + def find_or_create_object! + finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + finder_hash = parsed_relation_hash.slice(*finder_attributes) + + if label? + label = relation_class.find_or_initialize_by(finder_hash) + parsed_relation_hash.delete('priorities') if label.persisted? + + label.save! + label + else + relation_class.find_or_create_by(finder_hash) + end + end + + def label? + @relation_name.to_s.include?('label') + end end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 931d426c87f..2c0750c3377 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -15,6 +15,28 @@ "type": "ProjectLabel", "priorities": [ ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] } ], "issues": [ From c0e3183b734f9891fef347895bf1192e7a95221e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 10:34:44 +0100 Subject: [PATCH 106/206] add changelog --- changelogs/unreleased/fix-import-labels-error.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-import-labels-error.yml diff --git a/changelogs/unreleased/fix-import-labels-error.yml b/changelogs/unreleased/fix-import-labels-error.yml new file mode 100644 index 00000000000..86cae3a49ff --- /dev/null +++ b/changelogs/unreleased/fix-import-labels-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix project import label priorities error +merge_request: +author: From 6c81d03f53ec61cd76b56458b645ef318663ad69 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Thu, 8 Dec 2016 01:36:44 +0600 Subject: [PATCH 107/206] fixes left align issue for long system notes --- app/assets/stylesheets/pages/notes.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 106c5d4d390..6ac4ec6ea0d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,7 +43,7 @@ ul.notes { } .system-note-message { - display: inline-block; + display: inline; &::first-letter { text-transform: lowercase; @@ -55,7 +55,7 @@ ul.notes { } p { - display: inline-block; + display: inline; margin: 0; &::first-letter { @@ -151,6 +151,10 @@ ul.notes { } } } + + .note-headline-light { + display: inline; + } } .discussion-body { From 12d7652bcc94442f4159f9734eb9ecb560381d08 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Thu, 8 Dec 2016 01:40:38 +0600 Subject: [PATCH 108/206] adds changelog --- changelogs/unreleased/25368-fix-left-align-system-note.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25368-fix-left-align-system-note.yml diff --git a/changelogs/unreleased/25368-fix-left-align-system-note.yml b/changelogs/unreleased/25368-fix-left-align-system-note.yml new file mode 100644 index 00000000000..81fd0888773 --- /dev/null +++ b/changelogs/unreleased/25368-fix-left-align-system-note.yml @@ -0,0 +1,4 @@ +--- +title: Fixes left align issue for long system notes +merge_request: 7982 +author: From 359718603eb880bffc5688c16ceed170823b665a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Dec 2016 11:45:28 +0100 Subject: [PATCH 109/206] Ensure nil User-Agent doesn't break the CI API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../unreleased/25895-fix-headers-in-ci-api-helpers.yml | 4 ++++ lib/ci/api/helpers.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml diff --git a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml new file mode 100644 index 00000000000..b9a8e17c64a --- /dev/null +++ b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml @@ -0,0 +1,4 @@ +--- +title: Ensure nil User-Agent doesn't break the CI API +merge_request: +author: diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 31fbd1da108..5ff25a3a9b2 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -60,7 +60,7 @@ module Ci end def build_not_found! - if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) + if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) no_content! else not_found! diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 79f12ace999..3b5dc98e4d5 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -37,6 +37,11 @@ describe Ci::API::Builds do let(:user_agent) { 'Go-http-client/1.1' } it { expect(response).to have_http_status(404) } end + + context "when runner doesn't have a User-Agent" do + let(:user_agent) { nil } + it { expect(response).to have_http_status(404) } + end end context 'when there is a pending build' do From fd5062a848d63b89248c384e0171c6f1af833a49 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 11:50:31 +0100 Subject: [PATCH 110/206] update controller action to render error in form --- app/controllers/groups_controller.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 5c7709ea013..b83c3a872cf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,10 +82,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - error = group.errors.full_messages.first - alert_message = "Group '#{@group.name}' cannot be updated: " + error - - redirect_to edit_group_path(@group.reload), alert: alert_message + render action: "edit" end end From 55c61d2e412733b0feefcb5fa33a96e584ffe2bc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 11:53:44 +0100 Subject: [PATCH 111/206] Improve API specs --- .../mattermost_slash_commands_service.rb | 6 +- .../mattermost-slash-auto-config.yml | 4 + lib/mattermost/session.rb | 32 ++--- spec/lib/mattermost/command_spec.rb | 57 +++++++-- spec/lib/mattermost/team_spec.rb | 78 ++++++++---- .../mattermost_slash_commands_service_spec.rb | 119 +++++++++++++++--- 6 files changed, 229 insertions(+), 67 deletions(-) create mode 100644 changelogs/unreleased/mattermost-slash-auto-config.yml diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index fc1e7d79d08..6c78c0af71c 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -30,8 +30,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService def list_teams(user) Mattermost::Team.new(user).all - rescue Mattermost::Error - [] + rescue Mattermost::Error => e + [[], e.message] end private @@ -44,7 +44,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService auto_complete_desc: "Perform common operations on: #{pretty_project_name}", auto_complete_hint: '[help]', description: "Perform common operations on: #{pretty_project_name}", - display_name: "GitLab / #{pretty_project_name}", + display_name: "GitLab / #{pretty_project_name}", method: 'P', user_name: 'GitLab') end diff --git a/changelogs/unreleased/mattermost-slash-auto-config.yml b/changelogs/unreleased/mattermost-slash-auto-config.yml new file mode 100644 index 00000000000..43014d38769 --- /dev/null +++ b/changelogs/unreleased/mattermost-slash-auto-config.yml @@ -0,0 +1,4 @@ +--- +title: Allow to auto-configure Mattermost +merge_request: 8070 +author: diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index ddfeb88a71f..377cb7b1021 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,11 +1,11 @@ module Mattermost - class NoSessionError < Error + class NoSessionError < Mattermost::Error def message 'No session could be set up, is Mattermost configured with Single Sign On?' end end - class ConnectionError < Error; end + class ConnectionError < Mattermost::Error; end # This class' prime objective is to obtain a session token on a Mattermost # instance with SSO configured where this GitLab instance is the provider. @@ -36,12 +36,12 @@ module Mattermost def with_session with_lease do - raise NoSessionError unless create + raise Mattermost::NoSessionError unless create begin yield self rescue Errno::ECONNREFUSED - raise NoSessionError + raise Mattermost::NoSessionError ensure destroy end @@ -71,19 +71,15 @@ module Mattermost end def get(path, options = {}) - self.class.get(path, options.merge(headers: @headers)) - rescue HTTParty::Error => e - raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED => e - raise Mattermost::ConnectionError.new(e.message) + handle_exceptions do + self.class.get(path, options.merge(headers: @headers)) + end end def post(path, options = {}) - self.class.post(path, options.merge(headers: @headers)) - rescue HTTParty::Error => e - raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED - raise Mattermost::ConnectionError.new(e.message) + handle_exceptions do + self.class.post(path, options.merge(headers: @headers)) + end end private @@ -152,5 +148,13 @@ module Mattermost lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) lease.try_obtain end + + def handle_exceptions + yield + rescue HTTParty::Error => e + raise Mattermost::ConnectionError.new(e.message) + rescue Errno::ECONNREFUSED + raise Mattermost::ConnectionError.new(e.message) + end end end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index f70aee7f3e5..5ccf1100898 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -2,21 +2,60 @@ require 'spec_helper' describe Mattermost::Command do let(:params) { { 'token' => 'token', team_id: 'abc' } } - let(:user) { build(:user) } before do - Mattermost::Session.base_uri("http://mattermost.example.com") + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) end - subject { described_class.new(user) } - describe '#create' do - it 'interpolates the team id' do - allow(subject).to receive(:json_post). - with('/api/v3/teams/abc/commands/create', body: params.to_json). - and_return('token' => 'token') + let(:params) do + { team_id: 'abc', + trigger: 'gitlab' + } + end - subject.create(params) + subject { described_class.new(nil).create(params) } + + context 'for valid trigger word' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'returns a token' do + is_expected.to eq('token') + end + end + + context 'for error message' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') + end end end end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 704579f0f48..2d14be6bcc2 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -1,32 +1,66 @@ require 'spec_helper' describe Mattermost::Team do + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + describe '#all' do - let(:user) { build(:user) } - let(:response) do - [{ - "id" => "xiyro8huptfhdndadpz8r3wnbo", - "create_at" => 1482174222155, - "update_at" => 1482174222155, - "delete_at" => 0, - "display_name" => "chatops", - "name" => "chatops", - "email" => "admin@example.com", - "type" => "O", - "company_name" => "", - "allowed_domains" => "", - "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite" => false }] + subject { described_class.new(nil).all } + + context 'for valid request' do + let(:response) do + [{ + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] + end + + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: response.to_json + ) + end + + it 'returns a token' do + is_expected.to eq(response) + end end - subject { described_class.new(user) } + context 'for error message' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.team.list.app_error', + message: 'Cannot list teams.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end - before do - allow(subject).to receive(:json_get).and_return(response) - end - - it 'gets the teams' do - expect(subject.all.count).to be(1) + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') + end end end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 850ca45ddd8..d6f4fbd7265 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -3,40 +3,121 @@ require 'spec_helper' describe MattermostSlashCommandsService, :models do it_behaves_like "chat slash commands service" - describe '#configure' do + context 'Mattermost API' do let(:project) { create(:empty_project) } let(:service) { project.build_mattermost_slash_commands_service } let(:user) { create(:user)} - subject do - service.configure(user, team_id: 'abc', - trigger: 'gitlab', url: 'http://trigger.url', - icon_url: 'http://icon.url/icon.png') + before do + Mattermost::Session.base_uri("http://mattermost.example.com") + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) end - context 'the requests succeeds' do - before do - allow_any_instance_of(Mattermost::Command). - to receive(:json_post).and_return('token' => 'token') + describe '#configure' do + subject do + service.configure(user, team_id: 'abc', + trigger: 'gitlab', url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png') end - it 'saves the service' do - expect { subject }.to change { project.services.count }.by(1) + context 'the requests succeeds' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab', + url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png', + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{project.name_with_namespace}", + display_name: "GitLab / #{project.name_with_namespace}", + method: 'P', + user_name: 'GitLab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'saves the service' do + expect { subject }.to change { project.services.count }.by(1) + end + + it 'saves the token' do + subject + + expect(service.reload.token).to eq('token') + end end - it 'saves the token' do - subject + context 'an error is received' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end - expect(service.reload.token).to eq('token') + it 'shows error messages' do + succeeded, message = subject + + expect(succeeded).to be(false) + expect(message).to eq('This trigger word is already in use. Please choose another word.') + end end end - context 'an error is received' do - it 'shows error messages' do - succeeded, message = subject + describe '#list_teams' do + subject do + service.list_teams(user) + end - expect(succeeded).to be(false) - expect(message).to start_with("Failed to open TCP connection to") + context 'the requests succeeds' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: ['list'].to_json + ) + end + + it 'returns a list of teams' do + expect(subject).not_to be_empty + end + end + + context 'an error is received' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + message: 'Failed to get team list.' + }.to_json + ) + end + + it 'shows error messages' do + teams, message = subject + + expect(teams).to be_empty + expect(message).to eq('Failed to get team list.') + end end end end From 8b92e9c08b730b02816f501c88e3c237fa77d813 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 13:00:10 +0200 Subject: [PATCH 112/206] Allow projects with dashboard as path Signed-off-by: Dmitriy Zaporozhets --- app/validators/project_path_validator.rb | 3 ++- changelogs/unreleased/dz-whitelist-dashboard-project-path.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/dz-whitelist-dashboard-project-path.yml diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 927c67b65b0..d9ab8f167d8 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED + + RESERVED = (NamespaceValidator::RESERVED - + %w[dashboard] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml new file mode 100644 index 00000000000..2787a5c57df --- /dev/null +++ b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml @@ -0,0 +1,4 @@ +--- +title: Allow projects with 'dashboard' as path +merge_request: +author: From 21153f3e9e4af7cba2128f429b5f5267d52af58c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 20 Dec 2016 18:12:19 +0200 Subject: [PATCH 113/206] Rename groups with .git in the end of the path Signed-off-by: Dmitriy Zaporozhets --- .../unreleased/dz-rename-invalid-groups.yml | 4 + ...0141214_remove_dot_git_from_group_names.rb | 82 +++++++++++++++++++ db/schema.rb | 4 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/dz-rename-invalid-groups.yml create mode 100644 db/migrate/20161220141214_remove_dot_git_from_group_names.rb diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml new file mode 100644 index 00000000000..90af42da01c --- /dev/null +++ b/changelogs/unreleased/dz-rename-invalid-groups.yml @@ -0,0 +1,4 @@ +--- +title: Rename groups with .git in the end of the path +merge_request: 8199 +author: diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb new file mode 100644 index 00000000000..bd0e4b2cc07 --- /dev/null +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -0,0 +1,82 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDotGitFromGroupNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + invalid_groups.each do |group| + path_was = group['path'] + path_was_wildcard = quote_string("#{path_was}/%") + path = quote_string(rename_path(path_was)) + + move_namespace(group['id'], path_was, path) + + execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}" + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}" + + select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route| + new_path = "#{path}/#{route['path'].split('/').last}" + execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" + end + end + end + + def down + # nothing to do here + end + + private + + def invalid_groups + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'") + end + + def route_exists?(path) + select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? + end + + # Accepts invalid path like test.git and returns test_git or + # test_git1 if test_git already taken + def rename_path(path) + # To stay closer with original name and reduce risk of duplicates + # we rename suffix instead of removing it + path = path.sub(/\.git\z/, '_git') + + counter = 0 + base = path + + while route_exists?(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + + def move_namespace(group_id, path_was, path) + repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| + Gitlab.config.repositories.storages[row['repository_storage']] + end + + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + end +end diff --git a/db/schema.rb b/db/schema.rb index 14801b581e6..13a847827cc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161213172958) do +ActiveRecord::Schema.define(version: 20161220141214) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -854,7 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.datetime "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "scopes", default: "--- []\n", null: false + t.string "scopes", default: "--- []\n", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree From dfa842ce1de5b93bca2f336f3cf8c8769a555853 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 24 Nov 2016 15:45:34 +0000 Subject: [PATCH 114/206] Removes builds tab from merge request Fix specs --- app/assets/javascripts/merge_request_tabs.js | 416 ++++++++++++++++++ .../javascripts/merge_request_widget.js.es6 | 4 +- .../projects/merge_requests_controller.rb | 4 +- .../merge_requests/_new_submit.html.haml | 11 +- .../projects/merge_requests/_show.html.haml | 7 - .../merge_requests/widget/_show.html.haml | 2 - config/routes/project.rb | 1 - .../merge_requests_controller_spec.rb | 4 - .../merge_requests/created_from_fork_spec.rb | 8 +- 9 files changed, 425 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/merge_request_tabs.js diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js new file mode 100644 index 00000000000..0fc05421d75 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js @@ -0,0 +1,416 @@ +/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// +/*= require js.cookie */ + +// +// ### Example Markup +// +// +// +//
+//
+// Notes Content +//
+//
+// Commits Content +//
+//
+// Diffs Content +//
+//
+// +//
+//
+// Loading Animation +//
+//
+// +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MergeRequestTabs = (function() { + MergeRequestTabs.prototype.diffsLoaded = false; + + MergeRequestTabs.prototype.pipelinesLoaded = false; + + MergeRequestTabs.prototype.commitsLoaded = false; + + MergeRequestTabs.prototype.fixedLayoutPref = null; + + function MergeRequestTabs(opts) { + this.opts = opts != null ? opts : {}; + this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; + + this.setCurrentAction = bind(this.setCurrentAction, this); + this.tabShown = bind(this.tabShown, this); + this.showTab = bind(this.showTab, this); + // Store the `location` object, allowing for easier stubbing in tests + this._location = location; + this.bindEvents(); + this.activateTab(this.opts.action); + this.initAffix(); + } + + MergeRequestTabs.prototype.bindEvents = function() { + $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + $(document).on('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.unbindEvents = function() { + $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + $(document).off('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.showTab = function(event) { + event.preventDefault(); + return this.activateTab($(event.target).data('action')); + }; + + MergeRequestTabs.prototype.tabShown = function(event) { + var $target, action, navBarHeight; + $target = $(event.target); + action = $target.data('action'); + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } + navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo(".merge-request-details .merge-request-tabs", { + offset: -navBarHeight + }); + } else if (action === 'pipelines') { + this.loadPipelines($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else { + this.expandView(); + this.resetViewContainer(); + } + if (this.opts.setUrl) { + this.setCurrentAction(action); + } + }; + + MergeRequestTabs.prototype.scrollToElement = function(container) { + var $el, navBarHeight; + if (window.location.hash) { + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; + $el = $(container + " " + window.location.hash + ":not(.match)"); + if ($el.length) { + return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { + offset: -navBarHeight + }); + } + } + }; + + // Activate a tab based on the current action + MergeRequestTabs.prototype.activateTab = function(action) { + if (action === 'show') { + action = 'notes'; + } + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + }; + + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + MergeRequestTabs.prototype.setCurrentAction = function(action) { + var new_state; + // Normalize action, just to be safe + if (action === 'show') { + action = 'notes'; + } + this.currentAction = action; + // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' + new_state = this._location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (action !== 'notes') { + new_state += "/" + action; + } + // Ensure parameters and hash come along for the ride + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + }, document.title, new_state); + return new_state; + }; + + MergeRequestTabs.prototype.loadCommits = function(source) { + if (this.commitsLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#commits").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + _this.commitsLoaded = true; + return _this.scrollToElement("#commits"); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.loadDiff = function(source) { + if (this.diffsLoaded) { + return; + } + + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + var url = gl.utils.parseUrl(source); + + return this._get({ + url: (url.pathname + ".json") + this._location.search, + success: (function(_this) { + return function(data) { + $('#diffs').html(data.html); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + $('#diffs .diff-file').singleFileDiff(); + if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { + _this.expandViewContainer(); + } + _this.diffsLoaded = true; + var anchoredDiff = gl.utils.getLocationHash(); + if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { + _this.scrollToElement("#diffs"); + _this.highlighSelectedLine(); + }); + _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { + e.preventDefault(); + window.location.hash = $(e.currentTarget).attr('href'); + _this.highlighSelectedLine(); + return _this.scrollToElement("#diffs"); + }); + }; + })(this) + }); + }; + + MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { + var diffTitle = $('#file-path-' + anchoredDiff); + var diffFile = diffTitle.closest('.diff-file'); + var nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + diffFile.singleFileDiff(true, cb); + } else { + cb(); + } + }; + + MergeRequestTabs.prototype.highlighSelectedLine = function() { + var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; + $('.hll').removeClass('hll'); + locationHash = window.location.hash; + if (locationHash !== '') { + dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; + $diffLine = $(locationHash + ":not(.match)", $('#diffs')); + if (!$diffLine.is('tr')) { + $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString); + } else { + $diffLine = $diffLine.find('td'); + } + if ($diffLine.length) { + $diffLine.addClass('hll'); + diffLineTop = $diffLine.offset().top; + return navBarHeight = $('.navbar-gitlab').outerHeight(); + } + } + }; + + MergeRequestTabs.prototype.loadPipelines = function(source) { + if (this.pipelinesLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: function(data) { + $('#pipelines').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); + this.pipelinesLoaded = true; + return this.scrollToElement("#pipelines"); + }.bind(this) + }); + }; + + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + MergeRequestTabs.prototype.toggleLoading = function(status) { + return $('.mr-loading-status .loading').toggle(status); + }; + + MergeRequestTabs.prototype._get = function(options) { + var defaults; + defaults = { + beforeSend: (function(_this) { + return function() { + return _this.toggleLoading(true); + }; + })(this), + complete: (function(_this) { + return function() { + return _this.toggleLoading(false); + }; + })(this), + dataType: 'json', + type: 'GET' + }; + options = $.extend({}, defaults, options); + return $.ajax(options); + }; + + MergeRequestTabs.prototype.diffViewType = function() { + return $('.inline-parallel-buttons a.active').data('view-type'); + }; + + MergeRequestTabs.prototype.isDiffAction = function(action) { + return action === 'diffs' || action === 'new/diffs' + }; + + MergeRequestTabs.prototype.expandViewContainer = function() { + var $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + }; + + MergeRequestTabs.prototype.resetViewContainer = function() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } + }; + + MergeRequestTabs.prototype.shrinkView = function() { + var $gutterIcon; + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-right')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + // Wait until listeners are set + // Only when sidebar is expanded + }, 0); + }; + + MergeRequestTabs.prototype.expandView = function() { + var $gutterIcon; + if (Cookies.get('collapsed_gutter') === 'true') { + return; + } + $gutterIcon = $('.js-sidebar-toggle i:visible'); + return setTimeout(function() { + if ($gutterIcon.is('.fa-angle-double-left')) { + return $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + // Expand the issuable sidebar unless the user explicitly collapsed it + // Wait until listeners are set + // Only when sidebar is collapsed + }; + + MergeRequestTabs.prototype.initAffix = function () { + var $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + var $diffTabs = $('#diff-notes-app'), + $fixedNav = $('.navbar-fixed-top'), + $layoutNav = $('.layout-nav'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: function () { + var tabsTop = $diffTabs.offset().top - $tabs.height(); + tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); + + return tabsTop; + } + } + }).on('affix.bs.affix', function () { + $diffTabs.css({ + marginTop: $tabs.height() + }); + }).on('affix-top.bs.affix', function () { + $diffTabs.css({ + marginTop: '' + }); + }); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + }; + + return MergeRequestTabs; + + })(); + +}).call(this); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index e47047c4cca..bcfcf30c451 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -45,6 +45,7 @@ $('#modal_merge_info').modal({ show: false }); + this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); @@ -74,7 +75,7 @@ MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; - allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; + allowedPages = ['show', 'commits', 'pipelines', 'changes']; $(document).on('page:change.merge_request', (function(_this) { return function() { var page; @@ -173,7 +174,6 @@ message = message.replace('{{title}}', data.title); notify(title, message, _this.opts.gitlab_icon, function() { this.close(); - return Turbolinks.visit(_this.opts.builds_path); }); } } diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f0cb5a9d4b4..7b6cdba8dc8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 4a08ed045f4..349181be784 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -34,11 +34,6 @@ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size - - if @pipeline.present? - %li.builds-tab - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses_count %li.diffs-tab = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do Changes @@ -49,9 +44,6 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - - if @pipeline.present? - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" - if @pipelines.any? #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" @@ -66,6 +58,5 @@ }); :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}" + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7725558518f..d1fa51ae7ee 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -65,11 +65,6 @@ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do Pipelines %span.badge= @pipelines.size - - if @pipeline.present? - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses_count %li.diffs-tab = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do Changes @@ -98,8 +93,6 @@ #commits.commits.tab-pane - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - # This tab is always loaded via AJAX #diffs.diffs.tab-pane diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a8918c85dde..38328501ffd 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -24,12 +24,10 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, - builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; if (typeof merge_request_widget !== 'undefined') { - clearInterval(merge_request_widget.fetchBuildStatusInterval); merge_request_widget.cancelPolling(); merge_request_widget.clearEventListeners(); } diff --git a/config/routes/project.rb b/config/routes/project.rb index 335fccb617b..ddc7e955a27 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -92,7 +92,6 @@ constraints(ProjectUrlConstrainer.new) do get :diffs get :conflicts get :conflict_for_path - get :builds get :pipelines get :merge_check post :merge diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 9e0b80205d8..440b897ddc6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -649,10 +649,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET builds' do - it_behaves_like "loads labels", :builds - end - describe 'GET pipelines' do it_behaves_like "loads labels", :pipelines end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 142649297cc..73c5ef31edc 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -54,14 +54,14 @@ feature 'Merge request created from fork' do scenario 'user visits a pipelines page', js: true do visit_merge_request(merge_request) - page.within('.merge-request-tabs') { click_link 'Builds' } + page.within('.merge-request-tabs') { click_link 'Pipelines' } page.within('table.ci-table') do - expect(page).to have_content 'rspec' - expect(page).to have_content 'spinach' + expect(page).to have_content pipeline.status + expect(page).to have_content pipeline.id end - expect(find_link('Cancel running')[:href]) + expect(page.find('a.btn-remove')[:href]) .to include fork_project.path_with_namespace end end From 9a82aa70e3f3ff679c7a27ccbb9d497576b20822 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 25 Nov 2016 18:10:32 +0000 Subject: [PATCH 115/206] Remove builds tab from commit Remove unused file Fix commit link --- app/assets/javascripts/dispatcher.js.es6 | 1 - app/controllers/projects/commit_controller.rb | 31 ++----------------- .../projects/merge_requests_controller.rb | 11 ------- app/helpers/ci_status_helper.rb | 2 +- app/views/projects/_last_commit.html.haml | 3 +- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/commit/_builds.html.haml | 2 -- app/views/projects/commit/_ci_menu.html.haml | 4 --- app/views/projects/commit/builds.html.haml | 9 ------ .../merge_requests/show/_builds.html.haml | 1 - config/routes/project.rb | 1 - 11 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 app/views/projects/commit/_builds.html.haml delete mode 100644 app/views/projects/commit/builds.html.haml delete mode 100644 app/views/projects/merge_requests/show/_builds.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 752f35e6356..f45371f121e 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -138,7 +138,6 @@ new MergedButtons(); break; case 'projects:merge_requests:commits': - case 'projects:merge_requests:builds': new MergedButtons(); break; case 'projects:merge_requests:pipelines': diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 8197d9e4c99..3a5bf7d9ff5 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -8,13 +8,10 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] - before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] before_action :authorize_read_pipeline!, only: [:pipelines] - before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines] - before_action :define_status_vars, only: [:show, :builds, :pipelines] + before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] + before_action :define_status_vars, only: [:show, :pipelines] before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] @@ -35,25 +32,6 @@ class Projects::CommitController < Projects::ApplicationController def pipelines end - def builds - end - - def cancel_builds - ci_builds.running_or_pending.each(&:cancel) - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - - def retry_builds - ci_builds.latest.failed.each do |build| - if build.retryable? - Ci::Build.retry(build, current_user) - end - end - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) @@ -98,10 +76,6 @@ class Projects::CommitController < Projects::ApplicationController @noteable = @commit ||= @project.commit(params[:id]) end - def ci_builds - @ci_builds ||= Ci::Build.where(pipeline: pipelines) - end - def define_commit_vars return git_not_found! unless commit @@ -134,7 +108,6 @@ class Projects::CommitController < Projects::ApplicationController def define_status_vars @ci_pipelines = project.pipelines.where(sha: commit.sha) @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant - @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7b6cdba8dc8..3abebdfd032 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -201,17 +201,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def builds - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } - end - end - def pipelines @pipelines = @merge_request.all_pipelines diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index d9f5e01f0dc..94e91031680 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,7 +1,7 @@ module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project - builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) + # builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end # Is used by Commit and Merge Request Widget diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 7f530708947..e1fea8ccf3d 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,7 +1,8 @@ + - ref = local_assigns.fetch(:ref) - status = commit.status(ref) - if status - = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do + = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) = ci_label_for_status(status) diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 057a720a54a..b15be0d861d 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -7,7 +7,7 @@ = link_to pipeline_path(@build.pipeline) do %strong ##{@build.pipeline.id} for commit - = link_to ci_status_path(@build.pipeline) do + = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do %strong= @build.pipeline.short_sha from = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml deleted file mode 100644 index b7087749428..00000000000 --- a/app/views/projects/commit/_builds.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- @ci_pipelines.each do |pipeline| - = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index cbfd99ca448..13ab2253733 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -8,7 +8,3 @@ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Pipelines %span.badge= @ci_pipelines.count - = nav_link(path: 'commit#builds') do - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do - Builds - %span.badge= @statuses.count diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml deleted file mode 100644 index 077b2d2725b..00000000000 --- a/app/views/projects/commit/builds.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- @no_container = true -- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/head" - -%div{ class: container_class } - = render "commit_box" - - = render "ci_menu" - = render "builds" diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml deleted file mode 100644 index 808ef7fed27..00000000000 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true diff --git a/config/routes/project.rb b/config/routes/project.rb index ddc7e955a27..99b0afcada1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -32,7 +32,6 @@ constraints(ProjectUrlConstrainer.new) do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do member do get :branches - get :builds get :pipelines post :cancel_builds post :retry_builds From 7086dac42fff6e32bc38b389ffa104d9c21a159c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 29 Nov 2016 13:02:28 +0000 Subject: [PATCH 116/206] Changes after review Fix broken test Remove spinach tests for the builds tab --- app/controllers/projects/commit_controller.rb | 2 +- app/helpers/ci_status_helper.rb | 2 +- config/routes/project.rb | 2 -- features/project/commits/commits.feature | 2 -- features/steps/project/commits/commits.rb | 9 --------- spec/features/projects/commit/builds_spec.rb | 12 +++++++----- 6 files changed, 9 insertions(+), 20 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 3a5bf7d9ff5..791ed88db30 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -8,6 +8,7 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project + before_action :authorize_download_code! before_action :authorize_read_pipeline!, only: [:pipelines] before_action :commit before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] @@ -107,7 +108,6 @@ class Projects::CommitController < Projects::ApplicationController def define_status_vars @ci_pipelines = project.pipelines.where(sha: commit.sha) - @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant end def assign_change_commit_vars(mr_source_branch) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 94e91031680..94f3b480178 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,7 +1,7 @@ module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project - # builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) + namespace_project_pipeline_path(project.namespace, project, pipeline) end # Is used by Commit and Merge Request Widget diff --git a/config/routes/project.rb b/config/routes/project.rb index 99b0afcada1..2d261337594 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -33,8 +33,6 @@ constraints(ProjectUrlConstrainer.new) do member do get :branches get :pipelines - post :cancel_builds - post :retry_builds post :revert post :cherry_pick get :diff_for_path diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 1776c07e60e..3459cce03f9 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -47,8 +47,6 @@ Feature: Project Commits And repository contains ".gitlab-ci.yml" file When I click on commit link Then I see commit ci info - And I click status link - Then I see builds list Scenario: I browse commit with side-by-side diff view Given I click on commit link diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 007dfb67a77..18e267294e4 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Pipeline #1 for 570e7b2a pending" end - step 'I click status link' do - find('.commit-ci-menu').click_link "Builds" - end - - step 'I see builds list' do - expect(page).to have_content "Pipeline #1 for 570e7b2a pending" - expect(page).to have_content "1 build" - end - step 'I search "submodules" commits' do fill_in 'commits-search', with: 'submodules' end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index fcdf7870f34..33f1c323af1 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit builds' do +feature 'project commit pipelines' do given(:project) { create(:project) } background do @@ -16,11 +16,13 @@ feature 'project commit builds' do ref: 'master') end - scenario 'user views commit builds page' do - visit builds_namespace_project_commit_path(project.namespace, - project, project.commit.sha) + scenario 'user views commit pipelines page' do + visit pipelines_namespace_project_commit_path(project.namespace, project, project.commit.sha) - expect(page).to have_content('Builds') + page.within('.table-holder') do + expect(page).to have_content project.pipelines[0].status # pipeline status + expect(page).to have_content project.pipelines[0].id # pipeline ids + end end end end From bb992afef64b5e9e3e6d44767942c77b42079da2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 1 Dec 2016 11:51:44 +0000 Subject: [PATCH 117/206] Adds CHANGELOG entry --- changelogs/unreleased/23638-remove-builds-tab.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/23638-remove-builds-tab.yml diff --git a/changelogs/unreleased/23638-remove-builds-tab.yml b/changelogs/unreleased/23638-remove-builds-tab.yml new file mode 100644 index 00000000000..86d63208761 --- /dev/null +++ b/changelogs/unreleased/23638-remove-builds-tab.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "Remove Builds tab from Merge Requests and Commits" +merge_request: 7763 +author: From 5463ee66c850f5a31274a06bc27669ed127df811 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 23:26:14 +0000 Subject: [PATCH 118/206] Remove builds from merge request tabs file --- app/assets/javascripts/merge_request_tabs.js | 416 ------------------ .../javascripts/merge_request_tabs.js.es6 | 29 +- .../javascripts/merge_request_widget.js.es6 | 1 - 3 files changed, 3 insertions(+), 443 deletions(-) delete mode 100644 app/assets/javascripts/merge_request_tabs.js diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js deleted file mode 100644 index 0fc05421d75..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js +++ /dev/null @@ -1,416 +0,0 @@ -/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ -// MergeRequestTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the MergeRequests#show page. -// -/*= require js.cookie */ - -// -// ### Example Markup -// -// -// -//
-//
-// Notes Content -//
-//
-// Commits Content -//
-//
-// Diffs Content -//
-//
-// -//
-//
-// Loading Animation -//
-//
-// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.MergeRequestTabs = (function() { - MergeRequestTabs.prototype.diffsLoaded = false; - - MergeRequestTabs.prototype.pipelinesLoaded = false; - - MergeRequestTabs.prototype.commitsLoaded = false; - - MergeRequestTabs.prototype.fixedLayoutPref = null; - - function MergeRequestTabs(opts) { - this.opts = opts != null ? opts : {}; - this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; - - this.setCurrentAction = bind(this.setCurrentAction, this); - this.tabShown = bind(this.tabShown, this); - this.showTab = bind(this.showTab, this); - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - this.bindEvents(); - this.activateTab(this.opts.action); - this.initAffix(); - } - - MergeRequestTabs.prototype.bindEvents = function() { - $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).on('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.unbindEvents = function() { - $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).off('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.showTab = function(event) { - event.preventDefault(); - return this.activateTab($(event.target).data('action')); - }; - - MergeRequestTabs.prototype.tabShown = function(event) { - var $target, action, navBarHeight; - $target = $(event.target); - action = $target.data('action'); - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - navBarHeight = $('.navbar-gitlab').outerHeight(); - $.scrollTo(".merge-request-details .merge-request-tabs", { - offset: -navBarHeight - }); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else { - this.expandView(); - this.resetViewContainer(); - } - if (this.opts.setUrl) { - this.setCurrentAction(action); - } - }; - - MergeRequestTabs.prototype.scrollToElement = function(container) { - var $el, navBarHeight; - if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; - $el = $(container + " " + window.location.hash + ":not(.match)"); - if ($el.length) { - return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { - offset: -navBarHeight - }); - } - } - }; - - // Activate a tab based on the current action - MergeRequestTabs.prototype.activateTab = function(action) { - if (action === 'show') { - action = 'notes'; - } - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); - }; - - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - MergeRequestTabs.prototype.setCurrentAction = function(action) { - var new_state; - // Normalize action, just to be safe - if (action === 'show') { - action = 'notes'; - } - this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); - - // Append the new action if we're on a tab other than 'notes' - if (action !== 'notes') { - new_state += "/" + action; - } - // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; - history.replaceState({ - turbolinks: true, - url: new_state - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - }, document.title, new_state); - return new_state; - }; - - MergeRequestTabs.prototype.loadCommits = function(source) { - if (this.commitsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#commits").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - _this.commitsLoaded = true; - return _this.scrollToElement("#commits"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadDiff = function(source) { - if (this.diffsLoaded) { - return; - } - - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - var url = gl.utils.parseUrl(source); - - return this._get({ - url: (url.pathname + ".json") + this._location.search, - success: (function(_this) { - return function(data) { - $('#diffs').html(data.html); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { - _this.expandViewContainer(); - } - _this.diffsLoaded = true; - var anchoredDiff = gl.utils.getLocationHash(); - if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { - _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); - }); - _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { - e.preventDefault(); - window.location.hash = $(e.currentTarget).attr('href'); - _this.highlighSelectedLine(); - return _this.scrollToElement("#diffs"); - }); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { - var diffTitle = $('#file-path-' + anchoredDiff); - var diffFile = diffTitle.closest('.diff-file'); - var nothingHereBlock = $('.nothing-here-block:visible', diffFile); - if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { - cb(); - } - }; - - MergeRequestTabs.prototype.highlighSelectedLine = function() { - var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; - $('.hll').removeClass('hll'); - locationHash = window.location.hash; - if (locationHash !== '') { - dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; - $diffLine = $(locationHash + ":not(.match)", $('#diffs')); - if (!$diffLine.is('tr')) { - $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString); - } else { - $diffLine = $diffLine.find('td'); - } - if ($diffLine.length) { - $diffLine.addClass('hll'); - diffLineTop = $diffLine.offset().top; - return navBarHeight = $('.navbar-gitlab').outerHeight(); - } - } - }; - - MergeRequestTabs.prototype.loadPipelines = function(source) { - if (this.pipelinesLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: function(data) { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - return this.scrollToElement("#pipelines"); - }.bind(this) - }); - }; - - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - MergeRequestTabs.prototype.toggleLoading = function(status) { - return $('.mr-loading-status .loading').toggle(status); - }; - - MergeRequestTabs.prototype._get = function(options) { - var defaults; - defaults = { - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET' - }; - options = $.extend({}, defaults, options); - return $.ajax(options); - }; - - MergeRequestTabs.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - }; - - MergeRequestTabs.prototype.isDiffAction = function(action) { - return action === 'diffs' || action === 'new/diffs' - }; - - MergeRequestTabs.prototype.expandViewContainer = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); - }; - - MergeRequestTabs.prototype.resetViewContainer = function() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - }; - - MergeRequestTabs.prototype.shrinkView = function() { - var $gutterIcon; - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-right')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - // Wait until listeners are set - // Only when sidebar is expanded - }, 0); - }; - - MergeRequestTabs.prototype.expandView = function() { - var $gutterIcon; - if (Cookies.get('collapsed_gutter') === 'true') { - return; - } - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-left')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - // Expand the issuable sidebar unless the user explicitly collapsed it - // Wait until listeners are set - // Only when sidebar is collapsed - }; - - MergeRequestTabs.prototype.initAffix = function () { - var $tabs = $('.js-tabs-affix'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - - var $diffTabs = $('#diff-notes-app'), - $fixedNav = $('.navbar-fixed-top'), - $layoutNav = $('.layout-nav'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: function () { - var tabsTop = $diffTabs.offset().top - $tabs.height(); - tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); - - return tabsTop; - } - } - }).on('affix.bs.affix', function () { - $diffTabs.css({ - marginTop: $tabs.height() - }); - }).on('affix-top.bs.affix', function () { - $diffTabs.css({ - marginTop: '' - }); - }); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } - }; - - return MergeRequestTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 3ec0f1fd613..42015a02477 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -59,16 +59,13 @@ class MergeRequestTabs { - constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) { + constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.buildsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; this.setUrl = setUrl !== undefined ? setUrl : true; - this.buildsLoaded = buildsLoaded || false; - this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); @@ -119,10 +116,6 @@ $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'builds') { - this.loadBuilds($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else if (action === 'pipelines') { this.loadPipelines($target.attr('href')); this.expandView(); @@ -180,8 +173,8 @@ setCurrentAction(action) { this.currentAction = action === 'show' ? 'notes' : action; - // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' if (this.currentAction !== 'notes') { @@ -255,22 +248,6 @@ }); } - loadBuilds(source) { - if (this.buildsLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - document.querySelector('div#builds').innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - this.buildsLoaded = true; - new gl.Pipelines(); - this.scrollToElement('#builds'); - }, - }); - } - loadPipelines(source) { if (this.pipelinesLoaded) { return; diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index bcfcf30c451..0305aeb07d9 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -45,7 +45,6 @@ $('#modal_merge_info').modal({ show: false }); - this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); From 8db9888ffcfc98a1ceb8ea2592014522df927c4d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:18:57 +0000 Subject: [PATCH 119/206] Remove builds tabs --- app/assets/javascripts/dispatcher.js.es6 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index f45371f121e..14b37c6e412 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -167,9 +167,6 @@ container: '.js-pipeline-table', }); break; - case 'projects:commit:builds': - new gl.Pipelines(); - break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); @@ -186,7 +183,6 @@ container: '.js-pipeline-table', }); break; - case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; From 80d1ead188815282928762ddcc1bd69ed78f490f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:21:11 +0000 Subject: [PATCH 120/206] Resolve conflicts --- app/assets/javascripts/dispatcher.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 14b37c6e412..5245c5aa494 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -183,6 +183,7 @@ container: '.js-pipeline-table', }); break; + case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; From cfcefa154f4b94939dba9f3b56ff0ea001eb72c3 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 21 Dec 2016 11:35:26 +0000 Subject: [PATCH 121/206] Removed hack --- app/assets/javascripts/gfm_auto_complete.js.es6 | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 17d03c87bf5..64e6258c154 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -379,14 +379,7 @@ togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { this.setting.tabSelectsMatch = !isPrevented; this.setting.spaceSelectsMatch = !isPrevented; - const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`; - this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter); }, - preventSpaceTabEnter(e) { - const key = e.which || e.keyCode; - const preventables = [9, 13, 32]; - if (preventables.indexOf(key) > -1) e.preventDefault(); - } }; }).call(this); From 6023224db1b161164a0af968b7c42066c43dfaeb Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Wed, 21 Dec 2016 11:43:38 +0000 Subject: [PATCH 122/206] Simplify copy in issues empty state --- app/views/shared/empty_states/_issues.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 47379cfb4f8..b3c1128782f 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -13,7 +13,7 @@ The Issue Tracker is the place to add things that need to be improved or solved in a project %p Issues can be bugs, tasks or ideas to be discussed. - Besides, issues are searchable and filterable. + Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' - else From f93b876c6d0e580a9532b76855cba23f5fd5f3e7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:40:39 +0000 Subject: [PATCH 123/206] Adds CSS class to status icon on MR widget to prevent non-colored icon Adds MR id to changelor --- app/views/projects/merge_requests/widget/_heading.html.haml | 2 +- changelogs/unreleased/25898-ci-icon-color-mr.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25898-ci-icon-color-mr.yml diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 9ab7971b56c..5bc417d1760 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -17,7 +17,7 @@ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"} = ci_icon_for_status(status) %span CI build diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml new file mode 100644 index 00000000000..dd0f93e176f --- /dev/null +++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml @@ -0,0 +1,4 @@ +--- +title: Adds CSS class to status icon on MR widget to prevent non-colored icon +merge_request: 8219 +author: From 3bbe19d39bd7f2bebb1f68de99dfb791eaa7a928 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 20 Dec 2016 17:41:09 +0100 Subject: [PATCH 124/206] Backport moving MR widget CI JS out of script tag. --- .../merge_request_widget/ci_bundle.js.es6 | 24 +++++++++++++++++++ .../widget/open/_accept.html.haml | 21 +++------------- config/application.rb | 3 +++ 3 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 new file mode 100644 index 00000000000..02397561657 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -0,0 +1,24 @@ +$(() => { + /* TODO: This needs a better home, or should be refactored. It was previously contained + * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, + * but Vue chokes on script tags and prevents their execution. So it was moved here + * temporarily. + * */ + + $('.accept-mr-form').on('ajax:send', () => { + $('.accept-mr-form :input').disable(); + }); + + $('.accept_merge_request').on('click', () => { + $('.js-merge-button').html(' Merge in progress'); + }); + + $('.merge_when_build_succeeds').on('click', () => { + $('#merge_when_build_succeeds').val('1'); + }); + + $('.js-merge-dropdown a').on('click', (e) => { + e.preventDefault(); + $(this).closest('form').submit(); + }); +}); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index d6f7f23533c..7809e9c8c72 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,3 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| @@ -47,21 +50,3 @@ rows: 14, hint: true = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" - - :javascript - $('.accept-mr-form').on('ajax:send', function() { - $(".accept-mr-form :input").disable(); - }); - - $('.accept_merge_request').on('click', function() { - $('.js-merge-button').html(" Merge in progress"); - }); - - $('.merge_when_build_succeeds').on('click', function() { - $("#merge_when_build_succeeds").val("1"); - }); - - $('.js-merge-dropdown a').on('click', function(e) { - e.preventDefault(); - $(this).closest("form").submit(); - }); diff --git a/config/application.rb b/config/application.rb index 057d60ca869..7c7b858c607 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,6 +96,9 @@ module Gitlab config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" + config.assets.precompile << "lib/vue_resource.js" + config.assets.precompile << "merge_request_widget/ci_bundle.js" + config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" From 4ec259fd36fe57aa95446c10a47d784dae2c8f00 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 21 Dec 2016 11:44:47 +0100 Subject: [PATCH 125/206] Inject ::UploadedFile from Multipart middleware I mistakenly concluded Rack::Multipart injects File instances into the params. These should be UploadedFile instances. This reuses a mock UploadedFile class we already had in GitLab. --- lib/gitlab/middleware/multipart.rb | 8 ++++++-- spec/lib/gitlab/middleware/multipart_spec.rb | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index 65713e73a59..dd99f9bb7d7 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -42,7 +42,7 @@ module Gitlab key, value = parsed_field.first if value.nil? - value = File.open(tmp_path) + value = open_file(tmp_path) @open_files << value else value = decorate_params_value(value, @request.params[key], tmp_path) @@ -68,7 +68,7 @@ module Gitlab case path_value when nil - value_hash[path_key] = File.open(tmp_path) + value_hash[path_key] = open_file(tmp_path) @open_files << value_hash[path_key] value_hash when Hash @@ -78,6 +78,10 @@ module Gitlab raise "unexpected path value: #{path_value.inspect}" end end + + def open_file(path) + ::UploadedFile.new(path, File.basename(path), 'application/octet-stream') + end end def initialize(app) diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index ab1ab22795c..8d925460f01 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['file'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['user']['avatar'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['project']['milestone']['themesong'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end From 4c3ec579e5660daab23d48bd4b3e21050735f726 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 13:29:27 +0100 Subject: [PATCH 126/206] added more specs --- app/controllers/groups_controller.rb | 2 ++ spec/controllers/groups_controller_spec.rb | 4 ++-- spec/services/groups/update_service_spec.rb | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf..1e499199f82 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else + @group.reload + render action: "edit" end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4bb37bc52ee..98dfb3e5216 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -122,8 +122,8 @@ describe GroupsController do allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) post :update, id: group.to_param, group: { path: 'new_path' } - expect(response).to have_http_status(302) - expect(controller).to set_flash[:alert] + expect(assigns(:group).errors).not_to be_empty + expect(assigns(:group).path).not_to eq('new_path') end end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 8ac5736cbb3..531180e48a1 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -59,8 +59,6 @@ describe Groups::UpdateService, services: true do end it 'returns true' do - puts internal_group.errors.full_messages - expect(service.execute).to eq(true) end @@ -82,6 +80,10 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') end + + it "hasn't changed the path" do + expect { service.execute}.not_to change { internal_group.reload.path} + end end end end From a9f85d11a584a5a9ad99aaf6366f22fd6f6d0957 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 21 Dec 2016 11:03:52 +0000 Subject: [PATCH 127/206] Fix space issue and add test --- .../javascripts/gfm_auto_complete.js.es6 | 6 ----- spec/features/issues/gfm_autocomplete_spec.rb | 27 +++++++++++-------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 17d03c87bf5..5bfeed6f231 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -112,7 +112,6 @@ return value.path != null ? this.Emoji.template : this.Loading.template; }.bind(this), insertTpl: ':${name}:', - startWithSpace: false, skipSpecialCharacterTest: true, data: this.defaultLoadingData, callbacks: { @@ -129,7 +128,6 @@ }.bind(this), insertTpl: '${atwho-at}${username}', searchKey: 'search', - startWithSpace: false, alwaysHighlightFirst: true, skipSpecialCharacterTest: true, data: this.defaultLoadingData, @@ -172,7 +170,6 @@ }.bind(this), data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', - startWithSpace: false, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, @@ -200,7 +197,6 @@ displayTpl: function(value) { return value.title != null ? this.Milestones.template : this.Loading.template; }.bind(this), - startWithSpace: false, data: this.defaultLoadingData, callbacks: { matcher: this.DefaultOptions.matcher, @@ -225,7 +221,6 @@ at: '!', alias: 'mergerequests', searchKey: 'search', - startWithSpace: false, displayTpl: function(value) { return value.title != null ? this.Issues.template : this.Loading.template; }.bind(this), @@ -259,7 +254,6 @@ return this.isLoading(value) ? this.Loading.template : this.Labels.template; }.bind(this), insertTpl: '${atwho-at}${title}', - startWithSpace: false, callbacks: { matcher: this.DefaultOptions.matcher, sorter: this.DefaultOptions.sorter, diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index da64827b377..df3a467cbb7 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("~#{label.title[0]}") - sleep 1 note.click end @@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys("@#{user.username[0]}") - sleep 1 note.click end @@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do page.within '.timeline-content-form' do note.native.send_keys('') note.native.send_keys(":cartwheel") - sleep 1 note.click end @@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') end + it 'doesn\'t open autocomplete after non-word character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("@#{user.username[0..2]}!") + end + + expect(page).not_to have_selector('.atwho-view') + end + + it 'doesn\'t open autocomplete if there is no space before' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("hello:#{user.username[0..2]}") + end + + expect(page).not_to have_selector('.atwho-view') + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") @@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do end end end - - it 'doesnt open autocomplete after non-word character' do - page.within '.timeline-content-form' do - find('#note_note').native.send_keys("@#{user.username[0..2]}!") - end - - expect(page).not_to have_selector('.atwho-view') - end end From 9214d689204ef0c92bea8d97f9d5211d599df431 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 21 Dec 2016 13:34:24 +0100 Subject: [PATCH 128/206] Add new tests --- spec/lib/mattermost/client_spec.rb | 24 ++++++++++++++++++++++++ spec/lib/mattermost/session_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 spec/lib/mattermost/client_spec.rb diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 3c2eddbd221..74d12e37181 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do end end end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end end end From 804198ab7f1bdd854c64da729a04fc65f2b6ff7d Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 15 Dec 2016 10:07:41 -0500 Subject: [PATCH 129/206] Maintain milestone filter option when updating filter Setup teaspoon tests for Issuable --- app/assets/javascripts/issuable.js.es6 | 14 ++-- .../issuable/_milestone_dropdown.html.haml | 2 +- .../fixtures/issuable_filter.html.haml | 8 ++ spec/javascripts/issuable_spec.js.es6 | 81 +++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 spec/javascripts/fixtures/issuable_filter.html.haml create mode 100644 spec/javascripts/issuable_spec.js.es6 diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 1c10a7445bb..9c3c96c20ed 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,13 +1,13 @@ -/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ /* global Issuable */ /* global Turbolinks */ -(function() { +((global) => { var issuable_created; issuable_created = false; - this.Issuable = { + global.Issuable = { init: function() { Issuable.initTemplates(); Issuable.initSearch(); @@ -111,7 +111,11 @@ filterResults: (function(_this) { return function(form) { var formAction, formData, issuesUrl; - formData = form.serialize(); + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); @@ -184,4 +188,4 @@ } }; -}).call(this); +})(window); diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 40fe53e6a8d..415361f8fbf 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -3,7 +3,7 @@ - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") -- if selected.present? +- if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml new file mode 100644 index 00000000000..ae745b292e6 --- /dev/null +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -0,0 +1,8 @@ +%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} + %input{id: 'utf8', name: 'utf8', value: '✓'} + %input{id: 'check_all_issues', name: 'check_all_issues'} + %input{id: 'search', name: 'search'} + %input{id: 'author_id', name: 'author_id'} + %input{id: 'assignee_id', name: 'assignee_id'} + %input{id: 'milestone_title', name: 'milestone_title'} + %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 new file mode 100644 index 00000000000..d61601ee4fb --- /dev/null +++ b/spec/javascripts/issuable_spec.js.es6 @@ -0,0 +1,81 @@ +/* global Issuable */ +/* global Turbolinks */ + +//= require issuable +//= require turbolinks + +(() => { + const BASE_URL = '/user/project/issues?scope=all&state=closed'; + const DEFAULT_PARAMS = '&utf8=%E2%9C%93'; + + function updateForm(formValues, form) { + $.each(formValues, (id, value) => { + $(`#${id}`, form).val(value); + }); + } + + function resetForm(form) { + $('input[name!="utf8"]', form).each((index, input) => { + input.setAttribute('value', ''); + }); + } + + describe('Issuable', () => { + fixture.preload('issuable_filter'); + + beforeEach(() => { + fixture.load('issuable_filter'); + Issuable.init(); + }); + + it('should be defined', () => { + expect(window.Issuable).toBeDefined(); + }); + + describe('filtering', () => { + let $filtersForm; + + beforeEach(() => { + $filtersForm = $('.js-filter-form'); + fixture.load('issuable_filter'); + resetForm($filtersForm); + }); + + it('should contain only the default parameters', () => { + spyOn(Turbolinks, 'visit'); + + Issuable.filterResults($filtersForm); + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + }); + + it('should filter for the phrase "broken"', () => { + spyOn(Turbolinks, 'visit'); + + updateForm({ search: 'broken' }, $filtersForm); + Issuable.filterResults($filtersForm); + const params = `${DEFAULT_PARAMS}&search=broken`; + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + + it('should keep query parameters after modifying filter', () => { + spyOn(Turbolinks, 'visit'); + + // initial filter + updateForm({ milestone_title: 'v1.0' }, $filtersForm); + + Issuable.filterResults($filtersForm); + let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + + // update filter + updateForm({ label_name: 'Frontend' }, $filtersForm); + + Issuable.filterResults($filtersForm); + params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + }); + }); +})(); From 90c6a1a3198ba8090c645d740ac619e01a2e834e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Dec 2016 13:45:18 +0100 Subject: [PATCH 130/206] Use Grape's new Route methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Route#request_method instead of Route#route_method - Use Route#path instead of Route#route_path Signed-off-by: Rémy Coutable --- changelogs/unreleased/25908-fix-grape-after-update.yml | 4 ++++ lib/gitlab/metrics/rack_middleware.rb | 4 ++-- spec/lib/gitlab/metrics/rack_middleware_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/25908-fix-grape-after-update.yml diff --git a/changelogs/unreleased/25908-fix-grape-after-update.yml b/changelogs/unreleased/25908-fix-grape-after-update.yml new file mode 100644 index 00000000000..026d5592441 --- /dev/null +++ b/changelogs/unreleased/25908-fix-grape-after-update.yml @@ -0,0 +1,4 @@ +--- +title: Use Grape's new Route methods +merge_request: +author: diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 01c96a6fe96..91fb0bb317a 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -70,8 +70,8 @@ module Gitlab def tag_endpoint(trans, env) endpoint = env[ENDPOINT_KEY] - path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path] - trans.action = "Grape##{endpoint.route.route_method} #{path}" + path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path] + trans.action = "Grape##{endpoint.route.request_method} #{path}" end private diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index bcaffd27909..7371b578a48 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do end it 'tags a transaction with the method and path of the route in the grape endpoint' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint @@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do let(:transaction) { middleware.transaction_from_env(env) } it 'tags a transaction with the method and path of the route in the grape endpount' do - route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") + route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) env['api.endpoint'] = endpoint From dc303e4714b080924615ab5e61661e75fa87570c Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Wed, 21 Dec 2016 13:56:10 +0100 Subject: [PATCH 131/206] pipeline css changes --- app/assets/stylesheets/pages/pipelines.scss | 70 +++++++++++++-------- app/assets/stylesheets/pages/status.scss | 2 +- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 621b780ce4d..d834bc29e8f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -80,6 +80,10 @@ td { padding: 10px 8px; } + + .commit-link { + padding: 9px 8px 10px; + } } tbody { @@ -193,7 +197,7 @@ width: 8px; position: absolute; right: -7px; - bottom: 9px; + bottom: 10px; border-bottom: 2px solid $border-color; } } @@ -499,15 +503,10 @@ > .ci-action-icon-container { position: absolute; - right: 4px; + right: 5px; top: 5px; } - .ci-status-icon { - position: relative; - top: 1px; - } - .ci-status-icon svg { height: 20px; width: 20px; @@ -614,6 +613,10 @@ a { display: inline-block; + } + + .build-content { + width: 138px; &:hover { background-color: $stage-hover-bg; @@ -623,15 +626,24 @@ ul { max-height: 245px; overflow: auto; - margin: 5px 0; + margin: 3px 0; li { padding-top: 2px; - margin: 0 5px; + margin: 4px 7px; + padding: 0 3px; padding-left: 0; padding-bottom: 0; - margin-bottom: 0; - line-height: 1.2; + line-height: 0; + + .ci-action-icon-container:hover { + background-color: transparent; + } + + .ci-status-icon { + position: relative; + top: 2px; + } } } } @@ -680,11 +692,15 @@ .dropdown-build { color: $gl-text-color-light; + .build-content { + padding: 3px 7px 6px; + } + .ci-action-icon-container { padding: 0; font-size: 11px; float: right; - margin-top: 4px; + margin-top: 3px; display: inline-block; position: relative; @@ -694,16 +710,10 @@ } } - &:hover { - background-color: $stage-hover-bg; - border-radius: 3px; - color: $gl-text-color; - } - .ci-action-icon-container { i { - width: 25px; - height: 25px; + width: 24px; + height: 24px; &::before { top: 1px; @@ -740,6 +750,10 @@ margin: 0; } + .dropdown-build .build-content { + padding: 3px 7px 7px; + } + .builds-dropdown-loading { margin: 10px auto; width: 18px; @@ -788,19 +802,25 @@ .mini-pipeline-graph-icon-container .ci-status-icon { display: inline-block; border: 1px solid; - border-radius: 20px; + border-radius: 22px; margin-right: 1px; - width: 20px; - height: 20px; + width: 22px; + height: 22px; position: relative; z-index: 2; transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); svg { top: -1px; + left: -1px; } } +.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg { + width: 22px; + height: 22px; +} + .builds-dropdown { &:focus { outline: none; @@ -851,7 +871,7 @@ .mini-pipeline-graph-icon-container { .ci-status-icon:hover, .ci-status-icon:focus { - width: 28px; + width: 32px; padding: 0 8px 0 0; + .dropdown-caret { @@ -863,7 +883,7 @@ font-size: 11px; position: relative; top: 3px; - left: -11px; + left: -14px; margin-right: -6px; display: none; z-index: 2; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index a810ed32327..4acd17360c1 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,6 +1,6 @@ .container-fluid { .ci-status { - padding: 2px 7px; + padding: 2px 7px 4px; margin-right: 10px; border: 1px solid $gray-darker; white-space: nowrap; From 1ac06396778dec216cf0467a50c67040690656ca Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 21 Dec 2016 13:34:24 +0100 Subject: [PATCH 132/206] Add new tests --- .../projects/mattermosts_controller_spec.rb | 7 +++++- spec/lib/mattermost/client_spec.rb | 24 +++++++++++++++++++ spec/lib/mattermost/session_spec.rb | 24 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 spec/lib/mattermost/client_spec.rb diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index 3f9482b0cde..2ae635a1244 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -11,6 +11,9 @@ describe Projects::MattermostsController do describe 'GET #new' do before do + allow_any_instance_of(MattermostSlashCommandsService). + to receive(:list_teams).and_return([]) + get(:new, namespace_id: project.namespace.to_param, project_id: project.to_param) @@ -33,7 +36,9 @@ describe Projects::MattermostsController do context 'no request can be made to mattermost' do it 'shows the error' do - expect(controller).to set_flash[:alert].to /\AFailed to open TCP connection to/ + allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"]) + + expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project)) end end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 3c2eddbd221..74d12e37181 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do end end end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end end end From b902784dbff64d1f746fc38249a814debb8ed325 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 14:21:18 +0100 Subject: [PATCH 133/206] fix reset path --- app/controllers/groups_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 1e499199f82..efe9c001bcf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,7 +82,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - @group.reload + @group.reset_path! render action: "edit" end From 1812d523a316e3f495ae48504b48f3049628fe75 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 21 Dec 2016 14:43:39 +0100 Subject: [PATCH 134/206] Remove unused services from the database This adds a migration to remove unused services, where the properties are empty. As the properties are empty, those do not contain any settings or other information. Fixes #25727 --- changelogs/unreleased/zj-remove-unused-services.yml | 4 ++++ .../20161221140236_remove_unneeded_services.rb | 13 +++++++++++++ db/schema.rb | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/zj-remove-unused-services.yml create mode 100644 db/post_migrate/20161221140236_remove_unneeded_services.rb diff --git a/changelogs/unreleased/zj-remove-unused-services.yml b/changelogs/unreleased/zj-remove-unused-services.yml new file mode 100644 index 00000000000..8ede95f5faa --- /dev/null +++ b/changelogs/unreleased/zj-remove-unused-services.yml @@ -0,0 +1,4 @@ +--- +title: Remove unused and void services from the database +merge_request: +author: diff --git a/db/post_migrate/20161221140236_remove_unneeded_services.rb b/db/post_migrate/20161221140236_remove_unneeded_services.rb new file mode 100644 index 00000000000..a94ccc43a41 --- /dev/null +++ b/db/post_migrate/20161221140236_remove_unneeded_services.rb @@ -0,0 +1,13 @@ +class RemoveUnneededServices < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute("DELETE FROM services WHERE active = false AND properties = '{}';") + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 13a847827cc..05b6c807660 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161220141214) do +ActiveRecord::Schema.define(version: 20161221140236) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 5a4b3e8e302eaa263203d84e93862a89b5e90d90 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 21 Dec 2016 08:25:59 -0600 Subject: [PATCH 135/206] Increase code line height and decrease empty diff font size --- app/assets/stylesheets/framework/blocks.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 9f02749f5ab..e9aadffc73c 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -9,7 +9,7 @@ padding: 20px; color: $gl-gray; font-weight: normal; - font-size: 16px; + font-size: 14px; line-height: 36px; &.diff-collapsed { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d85d3f968d3..fcf7b8c2d36 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -134,7 +134,7 @@ $md-area-border: #ddd; * Code */ $code_font_size: 12px; -$code_line_height: 1.5; +$code_line_height: 1.6; /* * Padding From e1bf40e293409d974a8013685ec544c0f633ef16 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 16:43:27 +0200 Subject: [PATCH 136/206] Whitelist next project names: help, ci, admin, search Signed-off-by: Dmitriy Zaporozhets --- app/validators/project_path_validator.rb | 2 +- changelogs/unreleased/dz-whitelist-more-project-names.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/dz-whitelist-more-project-names.yml diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index d9ab8f167d8..79b2c99fd70 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator # 'tree' as project name and 'deploy_keys' as route. # RESERVED = (NamespaceValidator::RESERVED - - %w[dashboard] + + %w[dashboard help ci admin search] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/changelogs/unreleased/dz-whitelist-more-project-names.yml b/changelogs/unreleased/dz-whitelist-more-project-names.yml new file mode 100644 index 00000000000..4a3f1511a0b --- /dev/null +++ b/changelogs/unreleased/dz-whitelist-more-project-names.yml @@ -0,0 +1,4 @@ +--- +title: 'Whitelist next project names: help, ci, admin, search' +merge_request: 8227 +author: From 9809a9af8a3980f8a65262295cfd9701e793ac11 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 21 Dec 2016 16:21:55 +0100 Subject: [PATCH 137/206] Introduce "Set up autodeploy" button to help configure GitLab CI for deployment The button allows to choose a ".gitlab-ci.yml" template that automatically sets up the deployment of an application. The currently supported template is Kubernetes template. --- app/helpers/blob_helper.rb | 2 +- app/helpers/projects_helper.rb | 6 +- app/views/projects/show.html.haml | 4 ++ changelogs/unreleased/adam-auto-deploy.yml | 4 ++ doc/ci/README.md | 1 + doc/ci/autodeploy/img/autodeploy_button.png | Bin 0 -> 41799 bytes doc/ci/autodeploy/img/autodeploy_dropdown.png | Bin 0 -> 46761 bytes doc/ci/autodeploy/index.md | 39 ++++++++++++ lib/gitlab/template/gitlab_ci_yml_template.rb | 10 +++- spec/factories/projects.rb | 26 ++++---- spec/features/auto_deploy_spec.rb | 56 ++++++++++++++++++ spec/features/environment_spec.rb | 2 +- spec/features/environments_spec.rb | 2 +- spec/models/environment_spec.rb | 6 +- .../kubernetes_service_spec.rb | 2 +- spec/models/project_spec.rb | 2 +- spec/workers/reactive_caching_worker_spec.rb | 2 +- 17 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/adam-auto-deploy.yml create mode 100644 doc/ci/autodeploy/img/autodeploy_button.png create mode 100644 doc/ci/autodeploy/img/autodeploy_dropdown.png create mode 100644 doc/ci/autodeploy/index.md create mode 100644 spec/features/auto_deploy_spec.rb diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index f31d4fb897d..c3508443d8a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -188,7 +188,7 @@ module BlobHelper end def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names + @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) end def dockerfile_names diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d2177f683a1..7445f3c113c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -280,13 +280,15 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil) + def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, - commit_message: commit_message || "Add #{file_name.downcase}" + commit_message: commit_message || "Add #{file_name.downcase}", + target_branch: target_branch, + context: context ) end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8a214e1de58..a915c159cb4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -68,6 +68,10 @@ - if koding_enabled? && @repository.koding_yml.blank? %li.missing = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up autodeploy', target_branch: 'autodeploy', context: 'autodeploy') do + Set up autodeploy - if @repository.commit .project-last-commit{ class: container_class } diff --git a/changelogs/unreleased/adam-auto-deploy.yml b/changelogs/unreleased/adam-auto-deploy.yml new file mode 100644 index 00000000000..9d3348468d5 --- /dev/null +++ b/changelogs/unreleased/adam-auto-deploy.yml @@ -0,0 +1,4 @@ +--- +title: Introduce "Set up autodeploy" button to help configure GitLab CI for deployment +merge_request: 8135 +author: diff --git a/doc/ci/README.md b/doc/ci/README.md index 73bd2516d46..6a9495f8892 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -23,6 +23,7 @@ - [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [Review Apps](review_apps/index.md) - [Git submodules](git_submodules.md) Using Git submodules in your CI jobs +- [Autodeploy](autodeploy/index.md) ## Breaking changes diff --git a/doc/ci/autodeploy/img/autodeploy_button.png b/doc/ci/autodeploy/img/autodeploy_button.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2cd57a0bafb2abe820fd4970dea3db4cdfe17b GIT binary patch literal 41799 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 KrdO!z82ukLHJ5Mz literal 0 HcmV?d00001 diff --git a/doc/ci/autodeploy/img/autodeploy_dropdown.png b/doc/ci/autodeploy/img/autodeploy_dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..1486a8ec0ea6280b18f13002fb323dac4054035b GIT binary patch literal 46761 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

cyJ%lLGWmUmA@?o$iK=V$5mmsXla8Vo!l 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 literal 0 HcmV?d00001 diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md new file mode 100644 index 00000000000..503a00969d5 --- /dev/null +++ b/doc/ci/autodeploy/index.md @@ -0,0 +1,39 @@ +# Autodeploy + +> [Introduced][mr-8135] in GitLab 8.15. + +Autodeploy is an easy way to configure GitLab CI for the deployment of your +application. GitLab Community maintains a list of `.gitlab-ci.yml` +templates for various infrastructure providers and deployment scripts +powering them. These scripts are responsible for packaging your application, +setting up the infrastructure and spinning up necessary services (for +example a database). + +You can use [project services][project-services] to store credentials to +your infrastructure provider and they will be available during the +deployment. + +## Supported templates + +The list of supported autodeploy templates is available [here][autodeploy-templates]. + +## Configuration + +1. Enable a deployment [project service][project-services] to store your +credentials. For example, if you want to deploy to a Kubernetes cluster +you have to enable [Kubernetes service][kubernetes-service]. +1. Configure GitLab Runner to use [docker-in-docker executor][docker-in-docker]. +1. Navigate to the "Project" tab and click "Set up autodeploy" button. + ![Autodeploy button](img/autodeploy_button.png) +1. Select a template. + ![Dropdown with autodeploy templates](img/autodeploy_dropdown.png) +1. Commit your changes and create a merge request. +1. Test your deployment configuration using a [Review App][review-app] that was +created automatically for you. + +[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 +[project-services]: ../../project_services/project_services.md +[autodeploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy +[kubernetes-service]: ../../project_services/kubernetes.md +[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../review_apps/index.md diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb index 8d1a1ed54c9..d19b0a52043 100644 --- a/lib/gitlab/template/gitlab_ci_yml_template.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -13,8 +13,9 @@ module Gitlab def categories { - "General" => '', - "Pages" => 'Pages' + 'General' => '', + 'Pages' => 'Pages', + 'Autodeploy' => 'autodeploy' } end @@ -25,6 +26,11 @@ module Gitlab def finder(project = nil) Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories) end + + def dropdown_names(context) + categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages'] + super().slice(*categories) + end end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7fa834d7a2..f4ab732caa4 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -48,6 +48,19 @@ FactoryGirl.define do end end + trait :kubernetes do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com/api', + token: 'a' * 40, + } + ) + end + end + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -137,17 +150,4 @@ FactoryGirl.define do ) end end - - factory :kubernetes_project, parent: :empty_project do - after :create do |project| - project.create_kubernetes_service( - active: true, - properties: { - namespace: project.path, - api_url: 'https://kubernetes.example.com', - token: 'a' * 40, - } - ) - end - end end diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb new file mode 100644 index 00000000000..92c5b1cbb3b --- /dev/null +++ b/spec/features/auto_deploy_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'Auto deploy' do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :kubernetes) } + + before do + project.team << [user, :master] + login_as user + end + + context 'when no deployment service is active' do + before do + project.kubernetes_service.update!(active: false) + end + + it 'does not show a button to set up auto deploy' do + visit namespace_project_path(project.namespace, project) + expect(page).to have_no_content('Set up autodeploy') + end + end + + context 'when a deployment service is active' do + before do + project.kubernetes_service.update!(active: true) + visit namespace_project_path(project.namespace, project) + end + + it 'shows a button to set up auto deploy' do + expect(page).to have_link('Set up autodeploy') + end + + it 'includes Kubernetes as an available template', js: true do + click_link 'Set up autodeploy' + click_button 'Choose a GitLab CI Yaml template' + + within '.gitlab-ci-yml-selector' do + expect(page).to have_content('Kubernetes') + end + end + + it 'creates a merge request using "autodeploy" branch', js: true do + click_link 'Set up autodeploy' + click_button 'Choose a GitLab CI Yaml template' + within '.gitlab-ci-yml-selector' do + click_on 'Kubernetes' + end + wait_for_ajax + click_button 'Commit Changes' + + expect(page).to have_content('New Merge Request From autodeploy into master') + end + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 56f6cd2e095..c7411f1f4ac 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -93,7 +93,7 @@ feature 'Environment', :feature do end context 'with terminal' do - let(:project) { create(:kubernetes_project, :test_repo) } + let(:project) { create(:empty_project, :kubernetes, :test_repo) } context 'for project master' do let(:role) { :master } diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 72b984cfab8..e1387e44be8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -151,7 +151,7 @@ feature 'Environments page', :feature, :js do end context 'with terminal' do - let(:project) { create(:kubernetes_project, :test_repo) } + let(:project) { create(:empty_project, :kubernetes, :test_repo) } context 'for project master' do let(:role) { :master } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 93eb402e060..2aa63d7bcc3 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -200,7 +200,7 @@ describe Environment, models: true do context 'when the enviroment is available' do context 'with a deployment service' do - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } context 'and a deployment' do let!(:deployment) { create(:deployment, environment: environment) } @@ -218,14 +218,14 @@ describe Environment, models: true do end context 'when the environment is unavailable' do - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } before { environment.stop } it { is_expected.to be_falsy } end end describe '#terminals' do - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } subject { environment.terminals } context 'when the environment has terminals' do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 4f3cd14e941..0b20f4d3154 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do include KubernetesHelpers include ReactiveCachingHelpers - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } let(:service) { project.kubernetes_service } # We use Kubeclient to interactive with the Kubernetes API. It will diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 88d5d14f855..8be99bbf3ee 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1719,7 +1719,7 @@ describe Project, models: true do end context 'when project has a deployment service' do - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } it 'returns variables from this service' do expect(project.deployment_variables).to include( diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb index 5f4453c15d6..c6009e25713 100644 --- a/spec/workers/reactive_caching_worker_spec.rb +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ReactiveCachingWorker do - let(:project) { create(:kubernetes_project) } + let(:project) { create(:empty_project, :kubernetes) } let(:service) { project.deployment_service } subject { described_class.new.perform("KubernetesService", service.id) } From 68e1c3cbe6e9c6987b7e1d0e8bb23d69bf7e8f54 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 21 Dec 2016 16:32:00 +0100 Subject: [PATCH 138/206] Docs on MM auto config [ci skip] --- doc/project_services/mattermost_slash_commands.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 1a7c13a29b4..67cb88104c1 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -14,12 +14,18 @@ If you have the Omnibus GitLab package installed, Mattermost is already bundled in it. All you have to do is configure it. Read more in the [Omnibus GitLab Mattermost documentation][omnimmdocs]. -## Configuration +## Automated Configuration + +If Mattermost is installed on the same server as GitLab, the configuration process can be +done for you by GitLab. + +Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. + +## Manual Configuration The configuration consists of two parts. First you need to enable the slash commands in Mattermost and then enable the service in GitLab. - ### Step 1. Enable custom slash commands in Mattermost This step is only required when using a source install, omnibus installs will be From d8740cfe19ea033a119cb4530aeb41fcec4bafca Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 21 Dec 2016 17:30:29 +0100 Subject: [PATCH 139/206] Move javascript for widget check to ci_bundle. --- .../merge_request_widget/ci_bundle.js.es6 | 56 ++++++++++++------- .../widget/open/_check.html.haml | 9 +-- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 02397561657..2b074994b4a 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -1,24 +1,42 @@ -$(() => { - /* TODO: This needs a better home, or should be refactored. It was previously contained - * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, - * but Vue chokes on script tags and prevents their execution. So it was moved here - * temporarily. - * */ +/* global merge_request_widget */ - $('.accept-mr-form').on('ajax:send', () => { - $('.accept-mr-form :input').disable(); - }); +(() => { + $(() => { + /* TODO: This needs a better home, or should be refactored. It was previously contained + * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, + * but Vue chokes on script tags and prevents their execution. So it was moved here + * temporarily. + * */ - $('.accept_merge_request').on('click', () => { - $('.js-merge-button').html(' Merge in progress'); - }); + if ($('.accept-mr-form').length) { + $('.accept-mr-form').on('ajax:send', () => { + $('.accept-mr-form :input').disable(); + }); - $('.merge_when_build_succeeds').on('click', () => { - $('#merge_when_build_succeeds').val('1'); - }); + $('.accept_merge_request').on('click', () => { + $('.js-merge-button').html(' Merge in progress'); + }); - $('.js-merge-dropdown a').on('click', (e) => { - e.preventDefault(); - $(this).closest('form').submit(); + $('.merge_when_build_succeeds').on('click', () => { + $('#merge_when_build_succeeds').val('1'); + }); + + $('.js-merge-dropdown a').on('click', (e) => { + e.preventDefault(); + $(this).closest('form').submit(); + }); + } else if ($('.rebase-in-progress').length) { + merge_request_widget.rebaseInProgress(); + } else if ($('.rebase-mr-form').length) { + $('.rebase-mr-form').on('ajax:send', () => { + $('.rebase-mr-form :input').disable(); + }); + + $('.js-rebase-button').on('click', () => { + $('.js-rebase-button').html(" Rebase in progress"); + }); + } else { + merge_request_widget.getMergeStatus(); + } }); -}); +})(); diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index e16878ba513..50086767446 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,9 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + %strong = icon("spinner spin") Checking ability to merge automatically… - -:javascript - $(function() { - merge_request_widget.getMergeStatus(); - }); - From 15d83f6ae2e3b52a79e761a63c86907a6161acec Mon Sep 17 00:00:00 2001 From: Makoto Scott-Hinkle Date: Sat, 1 Oct 2016 13:53:08 -0700 Subject: [PATCH 140/206] Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. --- .../22742-filter-protocol-relative-urls.yml | 4 ++++ lib/banzai/filter/external_link_filter.rb | 2 +- .../lib/banzai/filter/external_link_filter_spec.rb | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/22742-filter-protocol-relative-urls.yml diff --git a/changelogs/unreleased/22742-filter-protocol-relative-urls.yml b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml new file mode 100644 index 00000000000..b331f5a4eb5 --- /dev/null +++ b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml @@ -0,0 +1,4 @@ +--- +title: 'Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742' +merge_request: 6635 +author: Makoto Scott-Hinkle diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 2f19b59e725..d67d466bce8 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -10,7 +10,7 @@ module Banzai node.set_attribute('href', href) end - if href =~ /\Ahttp(s)?:\/\// && external_url?(href) + if href =~ %r{\A(https?:)?//[^/]} && external_url?(href) node.set_attribute('rel', 'nofollow noreferrer') node.set_attribute('target', '_blank') end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 167397c736b..d9e4525cb28 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(filter(act).to_html).to eq(exp) end end + + context 'for protocol-relative links' do + let(:doc) { filter %q(

Google

) } + + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end + end end From 3c4d8441485dd0d82b58e2a15a51b2c00277bcc7 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 21 Dec 2016 16:43:29 +0000 Subject: [PATCH 141/206] Update test-and-deploy-ruby-application-to-heroku.md --- doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 08c10d391ea..42f15a27f12 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -34,7 +34,7 @@ production: This project has three jobs: 1. `test` - used to test Rails application, 2. `staging` - used to automatically deploy staging environment every push to `master` branch -3. `production` - used to automatically deploy production environmnet for every created tag +3. `production` - used to automatically deploy production environment for every created tag ### Store API keys You'll need to create two variables in `Project > Variables`: From 3d0074a6edc22eb6840bad639c8f4fde6be89293 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 12:20:18 +0000 Subject: [PATCH 142/206] Adds background color for disabled state to merge when succeeds dropdown Adds MR id to changelog entry Fix caret color Remove duplicated semicolon --- app/assets/stylesheets/pages/merge_requests.scss | 8 ++++++++ changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index e779e65eca3..394980704ae 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -21,6 +21,14 @@ display: inline-block; float: left; + .btn-success.dropdown-toggle .fa { + color: inherit; + } + + .btn-success.dropdown-toggle:disabled { + background-color: $gl-success; + } + .accept_merge_request { &.ci-pending, &.ci-running { diff --git a/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml new file mode 100644 index 00000000000..39ce0b66768 --- /dev/null +++ b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Adds background color for disabled state to merge when succeeds dropdown +merge_request: 8222 +author: From fd4e9c53ddc5c1c1ae98794937d479407511b0a3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 17:41:41 +0000 Subject: [PATCH 143/206] Use same font size for all items in issue title --- app/assets/stylesheets/pages/issuable.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4fac0cfb0ba..eeb5b590625 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -434,6 +434,7 @@ .issuable-meta { display: inline-block; line-height: 18px; + font-size: 14px; } .js-issuable-selector-wrap { From 52f255eb8fdee19239f8cb84affb911a6e784a91 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 17:57:56 +0000 Subject: [PATCH 144/206] Reduce MR widget title by one pixel --- app/assets/stylesheets/pages/merge_requests.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index e779e65eca3..dd938f88dae 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,7 +96,7 @@ .mr-widget-body { h4 { font-weight: 600; - font-size: 17px; + font-size: 16px; margin: 5px 0; color: $gl-gray-dark; From 7af884a5094b505d3b59c2d289237afd9148119c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 18:00:22 +0000 Subject: [PATCH 145/206] Adds entry to changelog Add MR id to changelog entry. Fix typo in changelog entry --- changelogs/unreleased/25906-title-size.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25906-title-size.yml diff --git a/changelogs/unreleased/25906-title-size.yml b/changelogs/unreleased/25906-title-size.yml new file mode 100644 index 00000000000..d92d06197e9 --- /dev/null +++ b/changelogs/unreleased/25906-title-size.yml @@ -0,0 +1,4 @@ +--- +title: Standardises font-size for titles in Issues, Merge Requests and Merge Request widget +merge_request: 8235 +author: From 27c613cb1818f5bca695f667feb5ba12eee19d59 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 21 Dec 2016 18:29:10 +0000 Subject: [PATCH 146/206] Remove unneeded bundle refs. --- config/application.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index 7c7b858c607..b5d89e9bafd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,9 +96,7 @@ module Gitlab config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" - config.assets.precompile << "lib/vue_resource.js" config.assets.precompile << "merge_request_widget/ci_bundle.js" - config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" From d0c5222de2c435f74ac7dfd99194afbd292f2dd2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 18:35:16 +0000 Subject: [PATCH 147/206] Put back progress bar CSS Add changelog entry --- app/assets/stylesheets/framework/tw_bootstrap.scss | 2 +- changelogs/unreleased/25938-progress-bar-gone.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25938-progress-bar-gone.yml diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 718dbbfea27..55bc325b858 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -33,7 +33,7 @@ @import "bootstrap/labels"; @import "bootstrap/badges"; @import "bootstrap/alerts"; -// @import "bootstrap/progress-bars"; +@import "bootstrap/progress-bars"; @import "bootstrap/list-group"; @import "bootstrap/wells"; @import "bootstrap/close"; diff --git a/changelogs/unreleased/25938-progress-bar-gone.yml b/changelogs/unreleased/25938-progress-bar-gone.yml new file mode 100644 index 00000000000..841c4d445c6 --- /dev/null +++ b/changelogs/unreleased/25938-progress-bar-gone.yml @@ -0,0 +1,4 @@ +--- +title: Adds back CSS for progress-bars +merge_request: 8237 +author: From 1c2d9015da095a9c4148cd5455e9ae4e8f854674 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 21:08:55 +0200 Subject: [PATCH 148/206] Whitelist next project names: notes, services Signed-off-by: Dmitriy Zaporozhets --- app/validators/project_path_validator.rb | 2 +- changelogs/unreleased/dz-whitelist-more-project-names-2.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/dz-whitelist-more-project-names-2.yml diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 79b2c99fd70..aec0d0ce44e 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator # 'tree' as project name and 'deploy_keys' as route. # RESERVED = (NamespaceValidator::RESERVED - - %w[dashboard help ci admin search] + + %w[dashboard help ci admin search notes services] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/changelogs/unreleased/dz-whitelist-more-project-names-2.yml b/changelogs/unreleased/dz-whitelist-more-project-names-2.yml new file mode 100644 index 00000000000..5d5f57d79e9 --- /dev/null +++ b/changelogs/unreleased/dz-whitelist-more-project-names-2.yml @@ -0,0 +1,4 @@ +--- +title: 'Whitelist next project names: notes, services' +merge_request: +author: From 3cb8f01b9a33925bd65347f4a97c6db2f787589c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 21:13:13 +0100 Subject: [PATCH 149/206] Added Autodeploy script for OpenShift --- spec/features/auto_deploy_spec.rb | 4 +- .../autodeploy/OpenShift.gitlab-ci.yml | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index 92c5b1cbb3b..e581aa411b0 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -37,7 +37,7 @@ describe 'Auto deploy' do click_button 'Choose a GitLab CI Yaml template' within '.gitlab-ci-yml-selector' do - expect(page).to have_content('Kubernetes') + expect(page).to have_content('OpenShift') end end @@ -45,7 +45,7 @@ describe 'Auto deploy' do click_link 'Set up autodeploy' click_button 'Choose a GitLab CI Yaml template' within '.gitlab-ci-yml-selector' do - click_on 'Kubernetes' + click_on 'OpenShift' end wait_for_ajax click_button 'Commit Changes' diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml new file mode 100644 index 00000000000..e384b585ae0 --- /dev/null +++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml @@ -0,0 +1,74 @@ +image: registry.gitlab.com/gitlab-examples/openshift-deploy + +variables: + # Application deployment domain + KUBE_DOMAIN: domain.example.com + +stages: + - build + - test + - review + - staging + - production + +build: + stage: build + script: + - command build + only: + - branches + +production: + stage: production + variables: + CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN + script: + - command deploy + environment: + name: production + url: http://production.$KUBE_DOMAIN + when: manual + only: + - master + +staging: + stage: staging + variables: + CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN + script: + - command deploy + environment: + name: staging + url: http://staging.$KUBE_DOMAIN + only: + - master + +review: + stage: review + variables: + CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + script: + - command deploy + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + on_stop: stop_review + only: + - branches + except: + - master + +stop_review: + stage: review + variables: + GIT_STRATEGY: none + script: + - command destroy + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + when: manual + only: + - branches + except: + - master From 4a1e1281ac862a0c86d62473ab09d559c7ec5485 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 21:25:23 +0100 Subject: [PATCH 150/206] Revert conflicting EE changes --- spec/factories/projects.rb | 26 +++++++++---------- spec/features/auto_deploy_spec.rb | 10 ++++++- spec/features/environment_spec.rb | 2 +- spec/features/environments_spec.rb | 2 +- spec/models/environment_spec.rb | 6 ++--- .../kubernetes_service_spec.rb | 2 +- spec/models/project_spec.rb | 2 +- spec/workers/reactive_caching_worker_spec.rb | 2 +- 8 files changed, 30 insertions(+), 22 deletions(-) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f4ab732caa4..f7fa834d7a2 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -48,19 +48,6 @@ FactoryGirl.define do end end - trait :kubernetes do - after :create do |project| - project.create_kubernetes_service( - active: true, - properties: { - namespace: project.path, - api_url: 'https://kubernetes.example.com/api', - token: 'a' * 40, - } - ) - end - end - # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -150,4 +137,17 @@ FactoryGirl.define do ) end end + + factory :kubernetes_project, parent: :empty_project do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com', + token: 'a' * 40, + } + ) + end + end end diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index e581aa411b0..92f1ab90881 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -4,9 +4,17 @@ describe 'Auto deploy' do include WaitForAjax let(:user) { create(:user) } - let(:project) { create(:project, :kubernetes) } + let(:project) { create(:project) } before do + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com', + token: 'a' * 40, + } + ) project.team << [user, :master] login_as user end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index c7411f1f4ac..56f6cd2e095 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -93,7 +93,7 @@ feature 'Environment', :feature do end context 'with terminal' do - let(:project) { create(:empty_project, :kubernetes, :test_repo) } + let(:project) { create(:kubernetes_project, :test_repo) } context 'for project master' do let(:role) { :master } diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index e1387e44be8..72b984cfab8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -151,7 +151,7 @@ feature 'Environments page', :feature, :js do end context 'with terminal' do - let(:project) { create(:empty_project, :kubernetes, :test_repo) } + let(:project) { create(:kubernetes_project, :test_repo) } context 'for project master' do let(:role) { :master } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 2aa63d7bcc3..93eb402e060 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -200,7 +200,7 @@ describe Environment, models: true do context 'when the enviroment is available' do context 'with a deployment service' do - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } context 'and a deployment' do let!(:deployment) { create(:deployment, environment: environment) } @@ -218,14 +218,14 @@ describe Environment, models: true do end context 'when the environment is unavailable' do - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } before { environment.stop } it { is_expected.to be_falsy } end end describe '#terminals' do - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } subject { environment.terminals } context 'when the environment has terminals' do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 0b20f4d3154..4f3cd14e941 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do include KubernetesHelpers include ReactiveCachingHelpers - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } let(:service) { project.kubernetes_service } # We use Kubeclient to interactive with the Kubernetes API. It will diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8be99bbf3ee..88d5d14f855 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1719,7 +1719,7 @@ describe Project, models: true do end context 'when project has a deployment service' do - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } it 'returns variables from this service' do expect(project.deployment_variables).to include( diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb index c6009e25713..5f4453c15d6 100644 --- a/spec/workers/reactive_caching_worker_spec.rb +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ReactiveCachingWorker do - let(:project) { create(:empty_project, :kubernetes) } + let(:project) { create(:kubernetes_project) } let(:service) { project.deployment_service } subject { described_class.new.perform("KubernetesService", service.id) } From ef99ffd76de31b109771dc091356b470629210fd Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:20:57 +0900 Subject: [PATCH 151/206] Rname katex.css to katex.scss --- vendor/assets/stylesheets/{katex.css => katex.scss} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vendor/assets/stylesheets/{katex.css => katex.scss} (100%) diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.scss similarity index 100% rename from vendor/assets/stylesheets/katex.css rename to vendor/assets/stylesheets/katex.scss From 796c4c07fd2f0065763c43ca1998db4426a20ee5 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:24:15 +0900 Subject: [PATCH 152/206] Replace url('...') to url(font-path('...')) --- vendor/assets/stylesheets/katex.scss | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/vendor/assets/stylesheets/katex.scss b/vendor/assets/stylesheets/katex.scss index 3e62df2329c..d300dc4c288 100644 --- a/vendor/assets/stylesheets/katex.scss +++ b/vendor/assets/stylesheets/katex.scss @@ -41,113 +41,113 @@ SOFTWARE. @font-face { font-family: 'KaTeX_AMS'; - src: url('KaTeX_AMS-Regular.eot'); - src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_AMS-Regular.eot')); + src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Caligraphic'; - src: url('KaTeX_Caligraphic-Bold.eot'); - src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype'); + src: url(font-path('KaTeX_Caligraphic-Bold.eot')); + src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Caligraphic'; - src: url('KaTeX_Caligraphic-Regular.eot'); - src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Caligraphic-Regular.eot')); + src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Fraktur'; - src: url('KaTeX_Fraktur-Bold.eot'); - src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype'); + src: url(font-path('KaTeX_Fraktur-Bold.eot')); + src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Fraktur'; - src: url('KaTeX_Fraktur-Regular.eot'); - src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Fraktur-Regular.eot')); + src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Main'; - src: url('KaTeX_Main-Bold.eot'); - src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype'); + src: url(font-path('KaTeX_Main-Bold.eot')); + src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Main'; - src: url('KaTeX_Main-Italic.eot'); - src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype'); + src: url(font-path('KaTeX_Main-Italic.eot')); + src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'KaTeX_Main'; - src: url('KaTeX_Main-Regular.eot'); - src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Main-Regular.eot')); + src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Math'; - src: url('KaTeX_Math-Italic.eot'); - src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype'); + src: url(font-path('KaTeX_Math-Italic.eot')); + src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'KaTeX_SansSerif'; - src: url('KaTeX_SansSerif-Regular.eot'); - src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_SansSerif-Regular.eot')); + src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Script'; - src: url('KaTeX_Script-Regular.eot'); - src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Script-Regular.eot')); + src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size1'; - src: url('KaTeX_Size1-Regular.eot'); - src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Size1-Regular.eot')); + src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size2'; - src: url('KaTeX_Size2-Regular.eot'); - src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Size2-Regular.eot')); + src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size3'; - src: url('KaTeX_Size3-Regular.eot'); - src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Size3-Regular.eot')); + src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size4'; - src: url('KaTeX_Size4-Regular.eot'); - src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Size4-Regular.eot')); + src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Typewriter'; - src: url('KaTeX_Typewriter-Regular.eot'); - src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype'); + src: url(font-path('KaTeX_Typewriter-Regular.eot')); + src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype'); font-weight: normal; font-style: normal; } From 21aefb44501041980b677822e1fce6a84f7be2fd Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:37:59 +0900 Subject: [PATCH 153/206] Add KaTeX fonts to assets paths and precompile --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index b5d89e9bafd..d36c6d5c92e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -83,6 +83,7 @@ module Gitlab # Enable the asset pipeline config.assets.enabled = true config.assets.paths << Gemojione.images_path + config.assets.paths << "vendor/assets/fonts" config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" @@ -108,6 +109,7 @@ module Gitlab config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" + config.assets.precompile << "vendor/assets/fonts/*" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' From 7afccbd121377c50b64f77ce0aebfeca512f4a7e Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Thu, 22 Dec 2016 00:14:36 +0900 Subject: [PATCH 154/206] Update build step for KaTeX. --- vendor/assets/javascripts/katex.js | 6 ++++-- vendor/assets/stylesheets/katex.scss | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js index beb31ca6a7e..6b59a3477a7 100644 --- a/vendor/assets/javascripts/katex.js +++ b/vendor/assets/javascripts/katex.js @@ -35,8 +35,10 @@ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. 2. make (requires node) - 3. sed -i 's,fonts/,,' build/katex.css - 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. + 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss + 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js, + build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and + fonts/* to gitlab/vendor/assets/fonts/. */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o build/katex.scss + 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js, + build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and + fonts/* to gitlab/vendor/assets/fonts/. */ @font-face { From ffe78adf040ac96e0763bdf556ce19ac966e480c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 21 Dec 2016 13:51:17 -0800 Subject: [PATCH 155/206] Update Bitbucket callback URL documentation Closes #25950 [ci skip] --- doc/integration/bitbucket.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 1dfc985eaea..2a14c0397ca 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -40,9 +40,13 @@ you to use. | :--- | :---------- | | **Name** | This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. | | **Application description** | Fill this in if you wish. | - | **Callback URL** | Leave blank. | + | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | + NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will + see an "Invalid redirect_uri" message. For more details, see [the + Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html). + And grant at least the following permissions: ``` From 0fab1b7fe3e7fd09ff5de429bb91d762d53f2b97 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Thu, 22 Dec 2016 00:23:26 +0100 Subject: [PATCH 156/206] Improve autodeploy docs [CI skip] --- doc/ci/autodeploy/img/autodeploy_dropdown.png | Bin 46761 -> 51420 bytes doc/ci/autodeploy/index.md | 7 ++++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/ci/autodeploy/img/autodeploy_dropdown.png b/doc/ci/autodeploy/img/autodeploy_dropdown.png index 1486a8ec0ea6280b18f13002fb323dac4054035b..d2733de83dffcd44c8b3c9c1be3582eb2e451108 100644 GIT binary patch literal 51420 zcmeFYbyS>7voA~t1oz+$!QF!gcMCdb@SuacgkZtloe)US;O-8C2X_xL5Ex)^?j(Dk zbKiZ>zW;yUT6fl(HP5tEcUS+ax~jUK2z6CC3{(A1$pVWaBv7UaB$BCkYB(` z-lY{rz`>yt*+@yLD@aLEsk;EJZ0s%J;N&AxbdmHlhY4~G-YUEgLY7BqN0~0WU`Lim zBBe&bRzwcUHv+afwy-PD^ACy74H!X^DbI!ocA^SD`TFa|VUO4`$IoFifbgBOyL z#`h#ygS-qqJTUtdc|VQ4?TAXcYD{PP%(Zx2r@=ZQLvIlnkjoh*diTUS_>_9gdNxG% z;``LJOt;2#w(07X#a@Ua*G?=MUQvkLn{w1M88KZej(*RX{_g7DEB|Z}aXp?c1UN*) zhCs%?@2QNM*~CXY(clG|430Xv-kTe0uHM%&$#{HZlx)}*q2U;VfXs0;;%);k?`KoA z{NL`ous>}oN?{Qe5B80E7h0oTJ8xpMNUr3j5t9oY-+kv?c5;DcX*l8>gV*qYt7Z@6@7`A%klu5L)w{6(_QgXW|=JdT-DT zP=}roe^{TJUqA{RmV;3 zyJ{@)%%p7b-WDGju&uZCn;1@F!@Ela>^+*$PK34w-;!jc5WTrzc!BpkV(+ua>v(qp zdz!8%ffn@z$UBd>>>m@zI{IlvsNJwBa2&B;g$Pq&iloXAH)M8m;68L^Bho)Zi6(fR zZG;-VC~Ov-^=2-%iR0wijnv6&ym@$?rstqOBZW7jeI4@CW^tt8H-l((Ph8eR&A7Ft^eL5OJ z5U%}E{gAzWC$@EM6WT%gIV&)SKy!NIoiuU@GedMm&zpnYsN09^ zcJLw#c(pKQB~&KQzM>rlhZS=D` z=!gSzopiLlaG7@sdd&1ad*)yC1a)F&O~-y2ne@KWqrAdl*h0?^HR!%1jvm}16Cuot z;}~Q)6ZLu?K+qZHx%qId3!js(^8Hm4jwCWl2yc(FBuYW{_b8_jC(M%^iXLH_grQfN zxH+3^%CD@j_p#IP1n`a`N72IDB;(FBXqK zMQ;ap+mt#|3gwlM8xNax}n3UexaeL_^3!P*_qv#{EEI!v%U|g`^L-Vfao9q zehJ0|W8|m`6!^|0{k*xrzaY6KzLmN~k_juUH~U1R>l|d3Vis&xvZX{9nOL0|H|_RC z@{7wCDMj6s+hnE@`jLPU``w!n?Gd>YAQRSCPAz+F8m$`b)Uspk=Av`ulp@ATD&`0CY@qE^~%$vd?lcU#&_B3R4SoiW^KSR4Po2v{xlWRsBYOP?kR5br{8D0_X zkHxR9vL$M1-;OG>3R+d6A&ylPQWKhSRgJ2rMQg69LZjg+{iW}6EH97jj*yO`E*p;; z53oqmN%}}$68|6-;VdF)Au1-RE7MSMAwBVipI`^}ZtAC;Y^{Q9!8R9mSzL@@u zUP+lmB{#!1JuICdb1rjD;B%ID#%`u|mXQEMW^1~2CXgT4_=b&)ZJMo=wNk$@RW;Rb zbS#y88+qGs5M@9+uGLbe_Pm<;dujb)jg@{lfDj;{-$@39t^NM+jBp^Z-nnkLX3@rV zLTA>a&>_L07HlVsCLC*&vg+>J;-CKje@B0(ivgoT^)(MLVhg@6^e~t#-`gvNQ6tEJ?ep({7Cq^uG=VyGobRFpzcijPX zg=Fn}j!;6d&N(iDXzCcBFutOD5xvJwC(t8ZpxhziW%#MZN&TAF%l%gcb`G|xyo&sj z^nL_{J{RLlsBE}Sm>6m;GF~X3nk0itcAdxcgVVN?gqJzOC*%k{(le@QUT%T&#^Ju< zgrqWh@rat3Oj391OdL^KAK*nR(Rz9c7u0ObtiPJgSAe$BLHTw0t-Xtf%auk@DV3%Zznp-zj%xViTRocOotE9o2{2)3 zq{l9`_sn_b`Rs;9iMBocqr!^z20&f=m#NhXNUB!L3dO2^ydk~a{%WSDtH)$-!}YbR z{@&q)5pSbi?nE0Hl5oQWF(4pfXEdIy<$S-HEAvApBPxKShDl%Z!o&G~YtJlaE3|)A z9PvTRpiqy7xw*d2e|YZ8_1nkk2Ft5ae)c>45w>#lu0uP9wFYPVS|hGsQq2{(EHzb%*6*4rPIINUolHX=Cyzw&=$Uzb?X z_`W?n-jqWa;C%DNDs8T^#>m72%|q!*8Z2nAZKzqR>w7w)U#DZZ+zLGGxO#ZJ%Q`GS zY?$Xc8pZtZeCzo{pvvRrUiV}$Teunyv_4z&enoXu6B*-Gt8EZf<@qqZpT4&99v869Iz{U9TS^XEdX54^iguj^1WNkDHpCfAy^@AYy2g)8F27?7ikx( zo>J$@^ii(8Pr;>aw6p9|$+pSKNa z;O{~?GN{glKcfddh3tCrW^^TuIs)O$2;s3`OfVX};|WWD;d^ieu#V#pi&Ho!X5E0(BvhQ-v&IZ14TG|2{$IiO zXe3(px21vDXE0X*#YrCE3I|6(_xt-y;q9wmFfSt4MoZ64PeobC9O%GqY5_E}WcPA# zg0+T&6Y&y)JvvyrnNoQ<*gLukd5O~eX(0rA{$0#LL-nVLo1G|)o{Bn^6wt+zijSR_ zos&ikm5Pc=#Kpo&=&iKu-`!#VL}}i;xj6}OaCmxpvU~Ed16`~+xC8|SIXJmFxVhP2 zE!bSW9o94sAOVPlAKadUEu{ORyNs{Z$o z|Irojzq|7C@&0Ge|ET%9rwGUI75vAF{$kgkQW(F)P(?WYoA+X)CNTM*hqGjMIl-&M&dHB;X@*UlpRX zMaW^CMIM)V>z)Tij8=BHQ?JogZuxK#afF4$f47KW4GXdCC26i9;s`;riw1}%V&hdV zzT?kKu?RJ!8!*?xr8_)({*f$!)(K(V=T>=43+guEHlaMBoCmQJRaWzKpVjOvtvtAe zYHA0sab|s&fP=^W0Ea*Y_YC>Xe>~<7qo7L;*`DL}i2S4cznjFtNfO{c``5lycnH`v z+#1S6DF3ASci#{3GUWd&_Mb>=lNGdZV5PQ7QFtD zgnwHK96U1M$G^@5E=U>s&CrN6m+7nj!GJ%6B?;XBbtZ5A-xB}-vx{B%`?8@S%xX zapmxYSO4BDGx~#%W&8D3RIWBMKt<_^wK$R*^F7 zn#}J`oGAY);rVaisLQ%OivFi`{x^Gr=;=Si&-R4|+x)vUBEsYmk>L2>6%Q{LY;N@I zWXi$+Qzyapf)OUK%-`Q({rjvUP^c{UoHyT*{2RkRya|Gn)CofL{r6d^D8rT%frd0g z1s4P#w4WDfe097?D=I2_K0pe5srnu1?>&E`iA}`&KEAG@VdSgE=bb)0wel#HTSKQ` z8xr0Rm-9!76cKNzqW^YOISR9IIbydkT9Id@ELMp zkDrp96!BiW?0h<2ZPm^7-Xr$DoYj@-FP~8ruDFoF&-K6AO%uf_`|?FbHkxP? z#p)1e;KeiKYm}ezFZ{XMfa_uhU@$&DZLIjCS71Jg|8_??&JcZMaqcC9LIX}s;v@|# z0+qThyim3}WteuK&~`Kr_V+`@yuQEjn}VY*ag-oOan{G`kD`5K8YX0~AX=~8MvKZtCUjWOGv(2B}U;jv;zbkn~1Z(DSwIT9`HA zY;yIKnQZl-pom4y!o<}B+XHvn@9sxBKr^W8xF8a+yhVe7`Y(>XxkUKEazxm99lFov zezj<=OId&y4DR|`Lb#@fN#sGT9`?a!_U9+0)q!YYO?tPQnU%C*FH0`B%u5C)EQ9sm zI>uP&=VXBtE1nK64ouMm>En6*>PBj3T!(f*XN7FH{tNO{mhf02zdoe(6e%RxX1-m) zR93;G<3_%~gNG>P3bLAi5}u z6jkCIeMxCUQ|f6>Rf~^rVAHhO+ny5$DKD}>!FqNd_X6vE|DiUyb>#+=j)cB(rY9I93W1aP63QL*;v4C!oOF%;ptk#2Kt(*CZy=iNKJ^2cut#gZu0+y^wDTvrkCEazm)G6{y&# zX)3>C+Ym8uOHft7papS0B9+v6TN4rvhfoSrWZHa^G=EzwywNb`_D&{oBG8}|(Mk8= z6VPC6OVcA4X$K}+i8=a94Jj(7{=)w7W)?=jR8}70FHHlJ^fyDcL_WA-Z(aq}g?wu2 zkt{OJ9zqDJJ5-wn4ORgE-{$>)YxB4nEH1C)CxSZc8)M&B2t7&1`@5J>cnDTeTj$p8?YDP=(JNoHC z{KW1`smeHJgb9_Or}T)Bv+pCap(KZh9WT-en}8xj*k~<}mzT`WRxgiL52pI+D!j6o z@^Brb%aLc$e=IY(R_f6kv+&twuC%$d^c5u)YiC5aYCvV@-dSo43BeAW`%SFCctT?@ z2{-W=hhH1rE=F9y0axb@lh zGAjxPYQ)*M+QpQ2T4%ZK8}BIGxix#=@Ghl_eBN7D_@g>_DREd8C4cj}?*#_wKXldQ z%Nid~VA`ySG4=9QGQaOuKVidN9ZRFf#%3SLaue= z@L^R>8D^>aK{G8(R8pn&K_Bj}S0vag=|TN5rywj2q$>nDg`C08h0JGKI^memRum zYCSrTO|Gfjo(UNA(S{1GF4K{AR=+**`_3}KW?$kz?bhqB?|sr+JThiy)pw}wF5aZG zx8h|Y)=WHKk@T%3i2MYQf9QBO3nEvXWmlZiOi$T_BP=_lZxO;;l~Src#B5?s znoxaP0)YB{egu>&L;_ycV3NJ2nm2Tf`DvZG{N1#7=eOzJ82yl`!9+ zmVA@fsZSrxz8WiIY;qNH*Det!GHQrl_qRE;RbD&Ll4sTIaO3131=ZJV zkRnQaZkr2}Old)WT&A}4O zPD6>ySxhE#na+6pyXrkn;T1X-b`z>)S0w%MES96n>~!(fWMOSo0Q>S!q2*-3s%8!M zb^%*Q{|6dSVSK&y1Vx6hr=t;7g<{Magg-WMDUL#&Ji3cTDxqxmWNOd(1d3bG1XU8L3LC z>=9vx2`c*ggkY9SguqUSF_a%tLoR`C_AlrSIhDlD2(6Xwsw<{_zZ-anb!eaHwMz)C zrqx=HC%Z$6uSIHqF6ez+d|0uRF86+hW4G1sy1RWU(7UK#FLp6hSt7j4K&m^sH{MY| z=HCpC?>{0KY1)VaZadGeF0-&zILJtiue8iNhLrC<2Mir@zF#c1_o?|LPQHNWtciw6hQkfhyP1FK=8 zeR(#IRp0yvsXc+?^Y3X~1N8e}#@+x0Y`fbDod1F;^O)3MFnxQ{eFQU5y4^CC88BX<+8?XkdnUOT}Y8xN-O+DH?fXbsB5R7sBWml}acRg#T`-qj3>T z$dCyS#Pf`g!UT<6WgrG!Ju6S(Y*PM4} zcX>pfy>4e;*{*|rQ`v*=Hu$YLhbitqA6!}VhSC7seAjuVczMcweEPoIF2$t3IU-{H zV-u;{=w!9C^R@b?xL_Hl={%qmYo7T2mVfQoSNkwqEE7-OULQ=jxR>4zG~rhq2u8@atq6= zmK4R5sU@D5@}nAT17+4tZ8vNTN`v3F%~Y>fS&VX)cQ@BX+2~&+1w^3({cE#DDy0VM z!z0`uFTOMy2n9mEiagjmZrPhZ8K{u}=~pO7SsqSGge;GMAdA&-5buI*;Ih16#dbS4 z|H;nsGuM?Wp=Y;Aah@YHzE{5C2KsU%!{&pd!zEf`#4UH~?9=QJU5+%AAXB?ajFwZ# z2r<8mPDRE6X=E{8kS_&5u5omM%$04Poo9dcaj=tZ)Nu?$`6!)Ti9>Xm5mA&zRS&}6 z>LGW5| zGF{Ok>@p1P!hFS2gkN&~Sjci)`*m{PJ5#fM)nX~&`Y9rux?@k?CNl}GiPinV>F!+2v?1;a!zV%0PFWa$~x- zqmai`s{bDG>3q)l$}@=YQtLYUuwQK(6cjOSyx07hLwxoj>yLXSZ|l$J&U6J-KWQxS zl#jZQouf5x{uU>!>~BwO#%y@vT|sh}So1#Y*KZfaJL*4Olpwdz@-DyE*5io=dn)X9 zwQBdM%I^<7?2E??7|+}BYtFuPgo_0B4M6Ow&+a2%j` zN3(nkC6{4X5fb(9eoOp#T4pKLCw;70sqc6w?UutDDdL$w>h*?g$oGv(@@^xDkJ~I` z%qh>juc|{Lj4kZ0MFe^n%G$Y~puZKl@~E(B0J`TnirmXvbbfl#Y&y1gkKO;yacMl8 z+9X3@mchrCsL@fm*|u^B%wp*}^znH^TMNa)yFFJHW&YFR8RscUz;*ST3>`*`W{+l@ z9jL!Mzh_;KN0-s7f>m^gSf_Q8eN$%>mkk%XE^%1JgkypX8W7dtNI&{b+dbNhMfCD` zv-@CqrN_g*T53I-?sa(Va&$~4BU)7!3pw>zgfB1|@1?!l^D{i^veYjpfz>z-A3kyL z(dN2&v#do%lbQGB&^-J4(HaBlIhLlksT4(Q)ijRIQ~q_8Q95b;sKq+rCg!Ivg{=TZ z(e7D&-jK=|Bbmc&$5^FvOxKNq(ItX&!8tEUHViLGy0RG> z@-!k~DVn0M=1tNS+KBlW_UIQ_=;lH9pfUHv$Uj$g;Lb|fAgHyf_igtt!K_4}q~*4Q zXZu+LskxBysB-x5T(`QFCr1)p<1#jXL;QrMRl2E!>?dbcx=qo|Hk6u&$85Pgyzo|o zozTs>VFdfcSpY49&J^bL$i$fPP(aq1G122>Lz{q!Ja&SD>u#!Dw!*d1ivmIfERs`@ z_JA-Cl~uE4%6$iVal$xs-Xzc{44s%pD4Ol07$W%%>R8dhiUt^|N|i#ZXMKUij|b3h zL)1axUHht(HkMrNNQ*0QqL{}=LS>;a@4pB2b}$)NZfzc~%W}kU4|lc6$K+|;?CDww z69#b((KVaKpQt-Q#W1C;Wdud%&FLHSz{-x41RbY(XW0?E(V@yPuDI+WQZSR}e7_^B<1+Y@)lNDOt?;zB~Kipw?-68JY z3=sE)|LM(G+>}slVfLMcR?uZX7PA94xU_8do@X#?+%`_+CQ8_1@%HYVt1%sOPN*^1 za@?K^VzOef-^sZy94*%aipym)533(q%UQPUjbX8D-7e7JJq-mgoxZJoYaBp2M_nr> ztT|30E~}4uoC1DokkAQOKe*3qR5$`H7x~Tkvw(@Kj|-c*Aro#3dQmu-9wQTvmh~&C z(4h>f(fJ9_wiZyfd0M96;CV#jXf&eh?!zgO&B_B*X@5d_sgZEnQGkMWG?;?+#xBZJ zXnrEW5_)yB@pH2CU>=kV&2wC6xEQHqXdG$m>}dE!U%x7H%nlVlCK%Lvy>%o)$`O;5 zMp&f?Kqhyc0CTn$N~kpw!<%|Jn$SkMd^8^|0_+uBw$mWRNZTSzb9o2V2~n5r3y^~P zxy%{-z zH<8|PDH&*$sjlmMZ?8`M^!Qmjzo`#atd=Mzgyyx$4|IO%Z^iQ4#VH@vaGxH)zs&eL zt}_pl2M+6rol(M(!3kmxHc?^AnM_g(^Nw--U^WJx^1&ZR!X4F;{QK>u&%v#uUB-@n z+r=gj?-`v&Xnj&p1G9Z4Ut@Wu#?VqDFi+(6=jTGa2v0XRua7!ntvJGg_uG`aXM>b! zQ=jEVug|veP~PEaC=!IS;(wnj2hcXu5~MU14a!9mQJ3WUlxD*M!W7#@^LE#jce?3> z+M(+j-XT0?t`l`DOkyf!X8$|{7LPFokEtIEwgaWw6FlDpN=;Lz%yg_b_E8bu-9t$pO zn|Ja>6H$F_742&v@|S+f2ajVp+}1Y(embiHswbp_8)WbN33oz}{L_UeO~c&SKv)Oo zox_<_QNUad35G@^`X`9}W4}mzX3FCV{{FcvKKNq9Uw}QazH`lT+98E_-ruPB1w$&I zrM}(`yIJB%q1PSebvTbq1r?FA^AFR;UNV}m7nv#9m3q^V7L>>4R1k=i$kiYA(aGLji~k52fO zA2_j%c{a8E@ONJ!L&?e;6~OUF!6|{~cRsYQ2Y;x#S0*UQ9PdG22K)@($os6UOabI~ zsF+~sck`>_C120BE~_eExtorfs53895hk!Ndk(hv^(Dwar~7Uosv+n}59jEYsXIQt z;7QZ=NF+d~uZBXN*{c1>=(JRb5Ed|MX>A=l{Nh?V?yH3W27}M%10UvH28gYt5t*F* z(pDPY<=Qh-IM`L%B*ZEt?H-E@8qjA8t$9=9eyV&iK|=D`7wh_@`i+b8QcM?}jLE8X z7V+rZ@Q=pDdElM^2w9o5qHD*9*d9vutOuW7Sx*3K1UTSc2C5CM;yS363Ebafi)RyE ztNO-j1U6yQeSYb-##EcOn_B@7E)yrGP0iT6M~e!BAmyG$A~cI>+MWUbimptH^nFvB zeSD$N1!>vF-QM(+Xl+7cdpVJ_nD+Vgm3F2lv_%{iXgmk;Of-cvuLa&Mud_4cxVg4G(4%RW)xRFpBG%(&+bQ5rVk?-+XvhqQICYyO~b_4F0lMj<5c2qMZ=3hJiyX% z^yvOG_A4y9G6$r|zV;2&<}u$cXl+bG>#6d-5!!h!zl1Y&Ab8WL=0-ScOlt!t>#Qd8 zSK17kt9cXoUYAtGz=p>H?F_n81zS~zUsKWw(l&H<7#C~yq&Mqqk@@^MlB!tk5m92% zYcaGEo5UB$WY zPz$)I7>Br%FFz7&4}Uk6O{p*&EsVb$VW}G8n>VDD zizWL;v;T#|ACk4sns)=TmKIJJZlvIe+tXD7eO@FqA7x6id@Qk69DL7mdqM7bbrQd=U9GMEyuX5BW|U!}Xd?mt z0hY6waOugtij?!7TE4o)n3c_nalWOaz|aw?;zw}ez^|-jP<(9%N1nsI~)_77&DqS?7x-;5MOplXogrLP`W|pNv6JVc6&X31r9Q>Htphg zeVvWk68&}E+=8{=5rXG8CFmq#C3iJSUAulG3dVN#?Wu=Qe z^UW&@_;{qaGrz0f9&d?zxMtSIv*NwpBl1C!$btyU=nC=t3XMNLTwxrYyTIte@W?h? z({PZdR{k-P>6nAU$)?Gmw9_NJTZ6H*=2?;DVF~`gfx%um)s?gL(O$7zHIpIv0pVwCRXbBhqAwA%NK${KH)A8xd+95fo z$#|--yW86eA7BBnk<;QVM_e!?#cOQyo{@BmA2Xw?9Z)KcM??pT9s^Y|8& zYYo~B_BR>!fg_ZSq{GQ}AW5<$)Z9A9)ry}0TEb@1xKj#OJb}BD2a-Q9yF2W-;N-ZS z%OLb;o^Thhn*-Stfo-xtMvuha@49h;$b~FUtsHhAA=WM4pKM)wL1(cUDQdo0SM}gK zmF2a&X^)%38VvtlwC9~SjzW0R){y&rYgaKG=*>7QFD_9)E|$*je+7vlqw`-51B1&Q za*oE(#(krVD1+UH!saK=ubKSWuG(-Ub!0 zwjeJoS-SGrekod6}c&Xc>W zOXW=04`TM9D|K6vdaMP4{IB2hNNwwcV#M&c4_tad1}C8onFpL|jpo^Y?)I4()}}2$ zGoZlOdrU`v6t|&XbY#&v+U3(`sgrAHg%t|q=Xk8K?NMUPK*h`#iZ#%7mc8M-!zw7q zTIgk&&TyZeK=M<|&0fn!(b8OMQy%+#CNhBD)bm-$#=(%r-u8AB?+=-vFZ|Up_oyF#B#fx?IdQhO7E+kFs2D6^(vzmyT)cj zz&J#!ah@vuw4dp+i`f0u6ocEJ05(y2nG-Nr**1{T$mo62yuUMQ z^5`O#X&x&m>)5pU;)#a(rWVB$B%)$8A;y-|xyUTu?C&}SkIXZMR>cHnen`J67_r8% z0~D~Ku9)+`IhkoUky?&UnWTIG!5a3!3pG3fvkiCeO3 zJ_egph@{{yY)b5BQ{|+?{urNGbnRPRPG;z!SP{>>_vs5jdw^@e*7U}%kl(T9D8Xp6 zM=mPm#MY#S+zNhBVb8+0=s=R?c#W_U>T~{b)fagd>6sG{8meRbEnbRuEs_PU`Md8E zD#|+F(f>Tf`{al9)iGS~mwK5-pYMkAk!U;rt*<$7`g8|mUSt$}8r>Um$Z=$2LQNLO_G9#zK}ij5 z|B6jt8ry1#^M8ke)A}n@^}-z1WGSjgMlX!l#)=@HQG+!aVUJ@*{gf)5Ts{6)SXi?B zA;qub_ONWta*)Ee{M02U@R;&(ld`C`_WMR}jKCTrtz0IehHXnm8lh)ZD*nk9^NlIU zG0R}FHjIz&M08`VssJ1o^Bq)jx56`{oWr^65UQ}Tto*2BG)>(~p~nR2D46NVN-5B- zNOFyH*U7}lsG)YG_)$0B3iE`+Z4_K#N2#0;wr`RiKV-q)f^(}Wk83V+v zH5@HJzd`CS#O?5A0y&LUWo>5Ch4e|fwGZGOL}oK2jw|gcOBxjSlAQ((R&Xc?gZA|I z$6A+GoY#0WL`>VFMLe3&#f!D|<*?Gm`CKi)QvQN9?!$W=#yNA7l{v!;t&sZAiXGVP z{p}A^a-q5*gdDk&eATX#_YBOdD3nnWQ_$p*f^pwh%VsnUgZ2@c9!|B zsQ+CFJLaimI^#IqNTG6$8w9uE6n5;LgjPuitMg+d#go#PilAo0Lt{p%r2C+|%gSqk zp&4!Kb_4M6o)-5V$Rtm6iXhb5@;RE+Vy?-Ma)w+lNq&c=S3sYctTxkNsj!zMB@cJd z(VkGW-Q6^kt6x|oyz-AXc~1a0zFuB_Pg9iP`OCvRzVZs^(`V2C3jEcn{5h6boW`ao5Xsp>Oy?Swf6#JE%?45bL%T+ z#?{oYRsBN3-c4^ExT1plJ|9qjwe-uHQy{0U0_VnEcPy)A!>V>cCOf?7(+~}2$zNG8 zAZFhtd!y2Bp!yneEn^Q^EnL!i+$y?UXFs~!1k(9wa{UBlTk-nVCqO-R^^c#hyqC9B z|46fmuZ-WNWPcT;s+fFs5~uY&=~(Qmw0otZ`}OLPFuOomkssq>2{)(d{o$@Zi(>|+ zzUj|p1Rm(<{ZO+BDv(XZS{O>t!#okX-9CM}A$xNeLkbZ;4)trvv3@$A3q4|4hj=Sz z%u=LP<Qyp@@#$~ zv?F2gaorzCzicsAQ#ERFB(WdDc6l|g%iziaeVs<&;g1#f2SPV}QtSu znZlc`sA>)l4%{}=uLN)3Z^s_lGrxVf-xr5Uv*MSkikwN`49STl=9MTTR+u~->K00- zttL048gw@u-xN+%I@GxIP830Z8WZzu;&DTy%%eVDW4=aHYH}zr@MQ6s8=~79z*}X% zJ)QsM(QEB~L0Idu5jjfe#Q}2bj^R*Upiyc|gD1BNN(kj1&zkAr984CEKAYs`_&Sc! zmLHjghWvE4o}MOMdO|xh-$knVhc8f}`Z11`9*%;{GEHVS@(fju;bW2iT`B zkn8rG^gK^|fm&%ua2ho9$1ZO@F=0ZR`>NEo7_H~}kO{!uquo2b7RGYgqXvHQ9vbbZ zN~&r$wo*Zjk#OW8KT&CRelq7UW>(@j_NAH|HgCQ1d9>o*ORk>Q@yb}SPP_382W$d| zp;5XoT-Iq?@GAB=K;R3JWhS+~)JNf)3rrgbg;;RG{klIE$H3aIps#;{A8_K{-hY#( zqbWxRgWCZXR|)X>7Lj`oY|utGs&|n!t*qxR4B}A3Nz%Uyod_Yf67Ywa%PqSzk(6y? zB+;STyiqVKz3$8)+7EBh{Rvgm)Ac!#(vMa33)THTVM_CQyK zXJ-2ud<<<+%`#*{oa1P|DsBDos|6YCCt;yHS7#i~`Mva990p?7QS z9MKuV%KI~*iHuz}CdxUoSX-qFRDqScqq*S=%}7ea7D`^ciS>oXc)4GVgYCv8R(eg2 z-`rlxZ5n)Hm_qG(v`=|R{c}ghX!lO~!)tdY6FFrv$961Pz#-s=5wR}eZPN_|n8J~M zgInR53KY{~*m`{>*sZyUfmdtGWcF?9_bK}3pQorh^(Z=#bNJ71-QMNV4 z2Z<`{_oXYTS!fEqFLaZfDYXhIKPkF#$LNl7^N380=T7n46yI<2Y5L)XK*d^R2-w<~ z#1rhCLPgi(suHxUjG=MdyD#V2+T|L`O=tvmbq^&f=qB0=1 zovoJk)?E4#hNicP6iMtn!}lM1P@QL;WrVHp9XWDlw^rFw|r;~n`k5^Q~|9{ z2|P?C;wGEbFXBiv_NToQebFyfRXbd&K|&nTE4-JR<0(-VpLq!1YGkyM?_p1M<&}K1 z;=U&Cs5z+}uiy|8PxAh7;tM9@?YQ>sqCL_a)g5jYn=587=W^eCG&Id5`snR+R7XGQ zvWI?i+x88ydYtTNv07n|wV6%)_(vmei}6eIqZZE$Rz09yNApIqP0hX1j8xl+PZ1=S zujW{XWj<5xAu0yj*|Z>f1!?%5HBA^(frKHZgls6(DC<$0*05m9$~oG3wNA5X;mGH%KZbuZa@2C^M!Uwb5dm>$ zMw;Vpp=HgO+jj%i3}z+53C23@;+!Se`!)6_hbgI7!{`0|wHDb9xk^UXJjS~vk$^a{ z+plW*3YvXyU27RFz>BqVd*eBz3U|QAJ>X6-<^8+Id+~=E@nKlbF+CC%1&ETR{&UZ4 zUj&{dERVb}scUd;MR56oMzb^Zsp6YS1EwI3yQo_hYg2&cN%{E?hPYlWD1+LPDqH>`6{_g7ZOw@v56xAmP8HOuHs0#Z76 zRs)P)=Qnv(XlR&C-Vx?`q+%8Rx}ehfPha>;V0o?QpZ;;jcM-~JUYOx)6Pn@6w68R6 z4f$Tp4X$EOjIFjxED@bh*uP?{ISVLBa*zxaeFt+$HF8)w!&UN=Y_`BH^Ii?*p5nwOAsdyXm^;+Wxu_We6M0{Y_B5s4*Q67Yc?&XdS4~&8@YKA^cVZ0%Y4rCL1h>2 zuTo?my83KxfYsyfo#lNx!Q$mz_-?GhNpj9jiqFRbLbPB9YtQh8mA@DQLrHbwDhja=D|Hk0vW4>G(d@Lz6(X z<`QW=)j_t)sWD$2k0D1g{j1Ok%=Aciai8X6w_k^T0K$jNEgmjCYrOyHSFYfg{`H*E z3H66vT2RcSpRK{=Sp>7lNlHa=&dNz$lERYjyt(#gPw-L8AaX+^0duo(dD#zl6156` zU67(d-`gyhXnd2KD-nM8-9ZqL~is& zJ)HjWAn)_7@RM5Ea3gI#C}~>_Ve7| zCj>>N)=-OU_i?tIW`0tJ(u#nDU6BOXYPRc3sP^+!r`2W4scd{)uW}bDuRAd1Z8B^7 z{mKU@AkMxcAlBY%Xnm8tWq}OYvto^|kvOhG1(6}|yV6)SCT$PfMU)c`HFz@zS{^Uc z_yXaT?^|&>mE>>R(QhF6^c3TB7!=LGzH%8^5cA-KaTOUMX>4Wg&m33LLU^ot|M?d8 z?g1;YaN^T)zByqQnrOVX_wyO26@zUVx#V6vV61g|P4?FS{`HwbK0P3sdy7%A@ejw4 zqK^#8;uH@AqFB1l3*TvrPgbuH7dN~_!$LxJLZNe*@r4DWWf@kz8_hG$ zcwY227lvxJ-PZMoN-`lg`GHt+7RtMYL5uYc|C>>FNheV;E9HgwC%^C9!tDb|SG5C4 z4OHoA9bF*T$By|=BnDm0s+6>UmTD0~``93!+5wy)tW2~a@77^pwXhug>&1)|K!t99 zg;L7Hi<^^(cpKc0YXO>LFJJQbX{z_tSGmBCIa&UG`(o`MhD*MqWU95zUzyabQyM@) z%R*4gv5TOm;dQT!X@3nthV$weH=hM2;sef_g-+uCV(+h`s@$UfQ5`^Pzhan7@! zXRX<5&d;2ycKwzT|G1mkh~2%8&ge=K*YB^jJuEZ7*O>`MBU0d4iu}7-K;uShI_^S9ks6`UOOy_%v5ZXXPYe`nH^+g${;}?`k<6MHo#^2 zbKS&8#-7%xnHx|T(0v}s|NUClmPPKa+)%f9ST18p7iSh;W@2LC_{oa; z$K(b$SHAgjCH-7N{4?vR<{Vcae#~BndrvZyl=^Wi+-sfYip9}kv`87Efp%NCX~m%y7*diDz6|CER{R%agI)xVd{G$_@{R9Sg% z7Q2VX;OviE-loK+LpnSy9S;*@eDRj}#XAxHoM)LiY)`g!TMZ5NN}nf{-_aPxucGm% zPjUY)g3b9>Hd>BZQM-L>qierQrSLp=@=&$-8%21N;zHMV9ARk+l1_#n)3q&Gfl5(d zITw!t2zY7`aAjt}r)Sno3lnacgHHE;QJeq}wB{}IUFV5vhpb8aHkz%N$~sx|o<#Wj z5zWLMmyiD14IX(jE#qDWA&;woNb8Hj-^n=r*6J>O_*;xLMt9AbY3=Fx9qs3DiEC>^ zF@~sDr8MVU=+$VymxhY774mGM2sTET>jwLSf0RZH$M@Y`f)BeJ3;!f+V#9^GzaAEy zK4>V;Kd96Yt6%U!WIDd%QM2@wASd#e1vNX>(l|@GMoNGJO zs9*Z;-l_1oCo^{Z7@Q4Ri2m2vaF+!6YGLnVkd%u!ckSMed>6o2ALml!4jq|WjZ|hc z^?MX!UK=hg(%Vb05)7!M{!ni|J@$S{>3jx4&mF^WpZoeERnw01`qPeOGNfb;}rO zok*rv$(Z97&f1+F<7x41gnL*wQuBpb%awoJnYMrRVn9O+L+Lj+ccy#f&6n@LzQ;hg zJ%;a*ALCV@1_mg)xmD3qQAnNnm6T7s6g(lIIOBWz7<>O=(HdfyOSHJFb*7(#JChpm z4tMm-xy8fri^g27Z~%*VM&n?$~c$(9>a*sT)}<7hd%tDNqAb*7I=`XULk zF3!@gUd*4Ju7r*=GR>&Vy3*Vu-XXe6pw(JGX%kn5i&fp*rBC)-Hl1{4vUSJ98UYxv zZo7fLg%OR31GRPa4Wd>Oq&{!I_{IW7M_r1fU07$JJhHtA0#;@ORlcq;{7lrLO>EH`S~$sRftfP433A-%E3jb9r&20R6+gL zMH8EVfUQRh)Bo;zc5+?UNU1JP`;&2zg-ctTdwuVs7j5DH^rHQrM#BGB zcY!{is?nT&{R>ZMG10*kG?8hx$^C1#M8zBoY zjm%4li4AnpK0`awKRwy*brYh#YyMct!Fsc6Z*TA8H$~HKSM7$t&udm^pJ+>&?-%1?}n6A%!t9q!eNZ zenWV$t}IjOD&q{`885=xr2JOOwkk zS8M4egjr5p20;q|YCQOniv?%&xoo2zjPb_SaogY6HI&YI8yz4RQ$7kkDe$xvQwV_R zh|0LA#TkAQs{Wk?89pTF3Rh`WXrWS!EZzScme%y7|CYCBSwg0qk^1BR`n5SxPiLn} zo(1kNlow(}l3z9IliW}xvH>mc$xRye%q%>j?gzznTb-QKge}Bh?xUk(-iK+l3Ihm} zX?p}iPv7|3+$}g&SU08OIIYK{?NHDjOb)dN2M_|Bm~f_%7jrp-*;O}nYBlCk>xQuY zBQZW}vBcXPSx5S*y!DPfT=&4SED>yQrCeuq*6+(;zBZte6yMmXA#ZrYXFt$9Ow0rt zi2i>DLWrZiRycaOQ_E|G*kSSazxsG4gSUm&z)kdkt3e1qm|=r=!|>2TnZ2`2nU+Vy z*AcEeEG5(ooLJn=V8RI=gSYVz;Tkjcl|V@_dN&P)V@Kvo8hZo3L-~`$drc-a9t(z^ z|8w8Gf@b2swMgD2#KhM5ve|9SbPxKRyfm19a`{##rk}xDHRs?+)A30r@Il*j?c$0# zA=ZD>7JP7tRKF-_XNjz2r~i(77DlAq^lJZZ6w70O!1JG=Q#p+hyirNEi6qDIy!|~B z#8xdP>s<9_^2;V~FxvEip4?r`ACYbxLL<9xx)ECSI zz%cDi7E9J12Jvs~ekYPK=5&oLp5+F3K_eSzG~Wz~!1jjWC-NBo$b*Wxqj=Hf-{319LsGJ4(fD079t<@&A_3I()J!zJjyA^p6C2?8gn^0( z3gs+dGH@o$2M=8hwcsZ}kBaubaC+egM3V|%+C)b{kis4 zzH*Tu4Lx=>d1d|bqnd7TUlH|MfHw&;Ol(gFFoiJ&@pP;a_1c?vKwRTNGko;&T+3_B z9Rf5dB1M0$QgqzZN&3~Tn| z+HD_i^lFUdpoQORX0yH$sp~MXY&<$~(arZHJDrq-^zC3GPlPL&E)dTv&4=Q%b^{X| z&ha!yj#OG}uPcW7KOLin*n!TK#D>F=C)9bJ6PRsN*i#&Muh{-1RYw{tKiHSg7O)=u z-6W+N?fs3z!&;lvv~b$%Xa9^GcooTwVreZEm2kGFV^}lmKcqwBX8lQi%}av;2dJ>l z-5VGTrUI!N^2BJ@yuCS1bI4+BG2z!ze|9il5zUE(kkvE?b(m9ep9mR+Mg`VRf@bEQ z4;;kL_tDY)t;(3DZzWaR`!_i(bK=bCGCKuSDLQ#F|C@6JOjogVjJQp83pNpwQk@zy z3K~*B%SQ~i3wcIVsQn@|(am^(iZWus06w{7hg@2VnNRtO3@ARWFd#4uw>5}x7&z}J z@BqS7LIX{{pi6EMk}rV(@;U;K{`^Wx2H$}AR*XIJI)D-p03z?EKBfc=Vg^DY&!6~j z+yQvG02r{Kc{l0-@Ok`k;3sczyni#?{sCfTyd6r(v#Vh_*%;u52vt@*KrmYHtre+w zen574@aFfH_fh}ZHx!sPG*-f#JqQ!{yW8L=*U3;iWQwj?LNi|2fFivwof^Y%Sv`1 zB+mYZe_C{`n$>RDimtN&>dl_{opvy66T*!iMtP1CI>l*BSNrQIQ z&J^B0UGFvz{MTPmG{x?Ly`2G-uAU?ju`h~&Z^J3>X4+P0MG{JD3G7sn0$~GZMR>Oc z_babw341J=k$Z{$%P;8D6K)0jz@+<~Y)hP%)}3s1H53nk zF{)MrnXYY1-p4;o&$?EN8hKKV2D99yWAJH)KG|M#%?p%^5qaPOCc8}6XC(_hCsZX; z4D?woHmc2eyF0z#FtW&C)0^`)!^?T#xH3JR>Bdu1=@@PSaX_Jbz@Sb9)>8=>pE7XZ zztGk+_rM~(HgViK zfLb0v^!&@Iy%USG{H+XI@rbDC;qS=g;&hs}UtuE3$`YrJ@%&cmV?yr+spddNKSvZq zu7V1cIVeY#?2lO~2H^p9t{h&|T($1%}k-FN@;)LVe2hQ{q6zyW}K})Q-!So`|R- zLZ=*iNJuI*Bq5*XoGsU@Bsdk7V$oq}AyHJb!l9yCJTuh`Rne2>%@-|S^^ToBQ5KRt z+p{|#PGI{+>mZRAD)ZG}Gf&HvF_FsdrIG)261Z;yHk06td+EtmY$I-Rr`L!kedA6B z?;{Xno#{-{kQhhVypoO@R94(lb|#HsZBJ+ExSdcr0gQx=uNb z%)eQ+ep4g8=&?T)E3vR)IdxRqcr2pw+9|w^2+46PHr2!Ss2`$nu}!qe`XieJ7t7s+!F@^CN!--9Jk2;B6Q-^j+oJUQEM+=szz zf)SHTrqjaj-3BxFn`)d^9|SV5WJ-rS=MUZGWH*)lN+fzV&+7$O7C$tdnY}OVT`!4# zw_FdM$|dg}es?&wZ|v$u_R&j2b$}t00}Z{U4&cFe{B-r$ zeCMrx{n!?cQk(dDyH_ZQS6QrMO-rFYH&cI5W($IA)`>2wYPk$BL~sn!ioX8A=~5r- zs8||Sy<9q@59@T*i$)Ibj&qO{QuKg8Pv(~KH4qlz08K7Wm8S-=#8=?RI3Bk7E6LN3 zGNcGQ<$jO5lm1k(#gB^wOZ<4O7qNY0**e}g@(ICVIi^%!-LO|)G`|>xjsN0FiL`8_ zOWiM;X_k*+0rvj%c42$m%#hf= zBl`p#9_5W6_yM*@L2_E5v&05dSTK>_9R1lkPnJp7ylq6K>`28jBZF37J`fR%#djGX zA#y^q0j3U1#Hu==aW0ti_cgi$?~MgPyv3fp&ulz3ON@u3ZQ@#y_xZ(lPx?)woS$sc zlQ?S>zY{!?`l;S#u95s_KTkvEfXKbomw-@FE;WaHfyNR_5Dc_Fwz0UX3Xf>Ums3`?v zk7NB7pb-&Fz`Jqt{h*;XJsvh(t>Sw1E$#ANL^}E=6*vg#<#LnF&*EE!B1>tVz9Z*L zg|L_bZCIb)vIn%uchLcP4Q6n#j83G`hGK2zR2tl=9ndl!U>uao0Q05U`YbK?V=0yX z6V1yJ#m)c%{Ki)zD^hh)>vy}02morS)`BboFJMwvors_8)MVb*)11 zjd5E|&igTh3byOYro`X1IEu?IPB&XC^j?Z_EuOAoj4c-WB8+L*miEdV=|9Hci+^CLEB{6m>n&eeQQ2fu#N&bL!Rpj zMGV;P5I7g+hS6z5!{>-Er6NL;s-j4B*%V#O_@zyJ3QwnDM=tUpS8sg5w6$uo|{Wce?)ur3DW2%idUP%SV$5a(eD~e z^(Jw0av6%n@NXEyVg3xEcgY`ye;w1e&(!cJwnuOA8wxRwORbj*=Q23_(|9&=ZXNl@ zKidef^x%&%3D_`iVB8AflBD<%K~3Z%&k@6Tut zi@AK8Bfe4LJ{#cJ&yoH#E&&ES7Ku3pk_KQ2ei6Q5u2_Bh3zA3R(Bq{t&*fGMN#}{b z%aIezQ=l({E)cXcQX-(9ryk4sh#^Tsx%sQydq8<&kcu^8wLe;Z87RJs#0&K-FT7r z^Hb8{Ptu#L`EqdXIn2zAZNzfo0}jjOTu*0OX2Hbq1C{d~m90#{`W$)F;yjw`+D{#e zMm%W5NiF6Feikd#Yq14M0H8?y{iuYku`2uYCR|uM7JqYqkEh&fibBvfQ4m%F zu@KfWDBaE>a9BTVznhCp;M77#XWrN6?qrD@0hBFRjq4Uk>3&2X^w!KFLf{S#C}L5k z0GmDx&WC38uo5~n3N~XAi=&{3=)cXa?rL`FDqcqehgfAKK|SZ6o1?JtOcI>ACb3$} zBVVvhg-xLHk|+nFuY8{7csTZAxj*9*=izdMunRsr=i@;}`~>I0ENndvM{rD=`HSB7 zXsuUxHJ_28Yj=3dKy{WZ$GltC{t;%1p(-H9 zvkD#k41GrleBS-DvzMC+mwd@XFP7~aQD{hhv(%CCFR#b4;$ie6fBxzn5=b5slLtDc zhDyKMZhG$5n$0%*HwYCPB9>WH^k0f4=D*blxzE4Otyl3|tXE_5;Gj){XB^A1@5$Vxy`tOi-2a9ceW!ELHvdk?FG9HiAti9>#3ygugrbB_3X8$k>7PSDwy z2nx5d$e?b(DCo>`xSB_ToJ3Z<&*fh6ukbi_c5DB&(--c^e{SHyKiPP;+SmzBjB-%Q zxxU5;OeS?pc19Eu$>0nXD)Ll6;HmnFHq~vXOFMHsy}vc{1_vLM|H?UHZ_{dV^-!5P z_vb&e6P!*9w&`|oJk}|ol&}Dzhg$(}sD~WbHhWH=U&DS~@qR-3A zU(M3Gt=;MC7@JaJQy&ou)z#ChXLwIp#Q79m%qKUL$ufOBEwaOFD>5Yt^c8)(`(m*t zDQ)`cu-jvQ2@=ymhet&O$~TKRd2=no@lgjq1%WxPcJl#Lw(Ukm?{yizE2g$exJ=py zp8Uj+Xb)5^u>&~6i-XU+)A4#`m)A*~*YDm{WpxJ5D#lO1i{9Jq>=#Z;z^~TmWpx<{ z8g$Cea?TxcQwqFqJ6$J4*SZ^kjl|!U#16>umx~d2xLOokS#xW^=xi%?FKYh~oSxb^ z$vD>@V!qfMyM+7ItrL{_Wy=^5d_w8M{=MD~gy?D-7y-thLerw9Vo{!dkAT~JJ zO=7WAg{{dU-#XRY8lP=5q_+lI1cQZX5TZE|8HSd^JN(aOACM$y_?1SMV~Dlpqf+ub z7r&!jj1!|-@6TmpGq@Prcw#05&cWMRiWfrkliJ|0wL7efr(1X-v%_v@Wn(Dw=Lehtv92FT)>$h{is;uYvq@`p|0OoLh9&y9 z^+qz8M%)sic}!2n?pUwqupn0LG+7~A5T|pMeJfTLV=%%@419hg>2vZ+XS+pS+#tX2 z`m6Z?mm43udCuFRzCMO$-`7LWq;115+G@ zF{6|!k|Mjp@Ny9e%^w^jx;(XO$dlHQ4cB^JNB=Lt&AA3@PWEjwJ$(1_jq84Ru=qH| zfYPE>-%H`0o+XL7v1xnKB0bNy_>>)1}MPnK4jAi@sxk*CSrTF*4gqlMRWrcDjf(s<_PH^chC zVOf)>;ozxVxwKoT=Ze$(ip=fDqpjQX%^+J#19Zf7u>*XFjkoaxY4d?i9%QPtm4)VS z&e)%(IixL5S~@$h`HnoZmgDY}m6fN-J^1;CVEl7kuQ38~yFhqBF^*SBMskyvra!O6 z*(tlUK}Yotr}dLNikn6t6)gVU_icu&RC_RoS{R6?;83_-PTXRMxpNn7TKw0OzBUnsGpmY@Vi6YJTxfBb2J$q`rn=^l1((RvYI@~l8!%< zeKAaHAf`U?2rn;D!Ko~LE_eQKYQ#K#`JiydUamzv_O+D)&2@7j4Lu$nm!yPz4Ub28 zVVYgOs{;(FjO7(F87qED@ClWAvIGxb7n>qaMgYqg1+#dOAf3(Gl@Yj3xP$?O>PU*= z13D-cNfNWOnBvLPn^K}FfgD<12M*>okh@Xv$kTL2dZ3QiL8Qi?O$9482q%t@;gixv+DFePKV&m|s-Lwi* z?qf38&}-ssVf!3Gu`->FI>?G#)LKA^te8f^FW*NGqg@Dggo8Sf%wW13OUyz?*ba?S zen&#Sa8+7}c!XB*D=;FuW3I&#Q?iM{c;c=f7Y24M*!U@!);tt0a0{xIL1FCemsq1= zbkf631=oI4qn-MDzvu;oRt)+;fN2ViKK_P{>u{CwZ)ZlG_$$-iY5La$lvsWe7OzuC z$6WI_03`ARv|nla7&5#GTDbWaDVvg?NGIaSg^8ouH0a&p6OiAJDQC$6tjS0M5MG*>= z^t#ws{+W6j;K~0aN`Rk%p67ei}i6p*y^jhr2#_%SEB|%-&=wf4ejZy4}-jd${8_e>XZvR&zeSFE? z$E0)*flT6Ga92om6gZR>IUwSm`H!xq^85HGUTo~TEe-u5;S%q~#)ga(qTD6vp8+=T zX&J|X)m%_PlJ$bSTC5=i>41xY7u=<0Uju@&3rvQl#PH`UXhTyI%f-g7RpLiDVD6Az zY;1*>MM0cC;tpT~1?TPWU^N$1ppU%ZE@$M|z&=86ZS|}*?d^8~jw&|ZZ;L!uBy*B;#4gnURYNAN%;OKT>d8z<6<~}fj-u+9_ zAloUVY8CFA45&215G{_E_?c6AODDLjQw8h=(jH9SN zs1H+VrDKLN^W`&UJdQg>$=UBx=erGP45zAGEUN8E~oS2dnys&Yp2=d1l8#^ZYR zRmiyKsr%}K8#4GWU6FudkTC}AI=K(18gf2{&#t@`N^RRYrdp6VHYl06eJvle{XZLu z$821W)X1h>!_s-v3Sc7!f`X4{-v2%yhc^7mSs(aOezMCAYgZfN!LH-_@cVOVw<71F zv9;x1p~bL0b)6|F)@%yP)-?V?dqul)7GwWgp_=6!r{bfYD-HV|swu^0d)?=nA#lhY zni(U({^MCMwC*t((>EFVT3A0RGM|1POXp#k@N@j7vsF;^aoO%AdZW@3e;!hY+#v_i z{S_(DQAs{eb|Vbo3^s4oiZxm(t_$(2_IlXi*Q zt<((@J(@LAbOavwbN-6SU>sIWH^dV-p1vdZeXCI5{B;Ue0TK!c{C;BM1B%gHQp}*w zm=JdTIz+Kmvkb;)#M2zhq1DrRhVVvr9ordudD^kl8fw0_(W`wy(GaznU*6q^1koXF zG%TCKo^Q*@@8rpTUwQR!!JFoU0KiCh*)+y+>Q+Ykk?Dj z$`oGMD%IE+i+gwl=}Aq7?a{i+Xi2=1^xWBD5L)e)nGd-U{yyD7I%Y9G`bKG8Rt$D4 z*Tmlzro7|~1zoUsO2~W4X;C@`-2fNOD1r$;igLKQaxsE*TFR}$uXTPGoxey~4Bbt~ z372W*(36Hiz5$YlR704?_oMAvBck2Tqv1d~at1}wJBo{4{jQ~TK{G?-02@JqwG6aT z2^636uyD{L>2F7Xn*mJ64oi+$T{fw-vZr-049VjE{_>!m!^o_}V;=lXa%uWed~gY0 z&m>JHM^}27`r%TIvzfZK;DiQeL^kHhX|ZgZj)l?D!YQ(0ahRx>QOp-EN8#xlgq|Vw z8T9A+6_m04jD{~+hr_hq+%I48og4&Sv5MySdlew6-U85@DQi(=SO*qdj-nS}X%wNN zQ*IO&WFt=f0XMvEJ%ZvNVleR;VE{%EH>hf}iXPMuAz#VhkdR4hJ>b#=w{2{5+RT5XCnnbl8O|>TRL%=Ak_FQ3z2xS4Jm`8*)E(a9dlT_ z!&ZbClWJGU(+K)68dAX0VOWjX4i}(>zxn>B**79X2FEFxD}Yo>A$Aog*BNvdkwg@u ztG8qT)@NSRdJ|D(>vp?zd*@5pFOHW+unC2MklQpUo+;0+*#%t6q)yS!3Zo}f0MX}7 z91LKP5zsLEj69OP?)Uz3TJ@bVj#nBXuT0NwO7mi}*l|~yM(Zue*0Vv``co*tJLi1z z3g+{eC_5{{sVQOY6tDW-C;#|dCh~tV7l`EZBX)V6Eg7VTB{@8|0H~*x9)Q5E5lcab z5fJxfw8g|-nb&q&K$^8^3Q|DHT>xIwG@AYl$|o@J7`peL|K#Bxv_v8vsLE4t9a1oA z5IvAy1PCztpDBL7QZiF$fqR{}Ay6O&rCNCH)PZuKfydA!53VqWP2K?2C6F4D@*RBi z4*1en?+1i{OJX3(TP|S%aJHk9^L`lDm#a$=pWgRkt56(Sd(u z=TBq6;u^Vi`2ZgV--5qGC5iFCS%ks|j8~mGSrDB6N@nyf=`TP+bOH#2n3{rYF5T&p z+|%uI2&B{lKxppzAUkLzz7ZCNFLb&yu)R2Xg*!{FfZ@}MvR?pBKvg>yN={DO6`$Q} zgbJHy-$JPt-DWi=22V>EOb=cEfaIjNWCU141m+ABLJ${Ahk4Qpy<$FhIrw9;ys@$O z+{(^Q#dLs*wJbLv*!nN z=S;l!#vJCwCI0vjw8!0f_B&eJ@I8djab>Xi-TrRQP4DXbGsv!u4ai@gmTjO?xK^gNiDJeFZG zCPA6=T49}0wezD({KIi$d}hksQQd>1%+1 z7bO+dR4@tW$q+yDkofGY24eu1LE9u%(i5$LOeF^} z9>N|E4{zfwQ`k$9wPKkaAx^*|h)+N2n)2bLllzV5rk z;xAJGqPx*>yuo>DTQYB-?XcLhAu@9qMkVfEwLV-}6gXWyzn`MVx@Rv|%U?yCM5Vvq z)8n-KTYs^?@i>W6Kj&SB1b5xaIlwy{GF1S>l_l)BGI;AKeTJ=UG|#KW2&_X=0e#I= zYgwq|ljE=HSBuJ4vh1X2n5IvfY`GELuYhs#@}gre|4d@&b?QVnyd2vxK&A*! z9{>WX4&kX`KcX`S$S%io2qU!CXtll2?EnFY(r?=UT-t`j*)NICGpcb?bl?1nC3`M3 z%$ri)^m&*Ck7kNJ29u1<;L)TdgMnUtxN#D+4TKdEa*3T3X zOP`nFVEXkLe%MC ze5}wC<-%#jnROdIJ)YuNZfSJ0nK3*OWyh9bPNL6|i%GgKS}|$5InG8+c^}*xl4Ug9 zBD<|`^*TLTXp*fiAWZG`J(FfX*7|SF4kck1##2%Y2KA*#Y^HbSVNC zI~J8o#S~IWXiY#@8wy(VP#4tj7}QjO?w4Vw_3@!bg7g00e<8?VLfH(M(Y(o47>WV^ zYp0Cc`$ZTsYW|4)ptkz4|^;6_^ZnO8tRj6Lg7BLEzxr;6g7dQ1j(Fx68u659*bt$GP~nUzA~R?xv@J zv+SQyq_h|kk`$OCXnpEnaBLsvi5G%1dHIQjzFY}UIBZ`d ze7x7HFpm>%LIXKS6dx`u(UU(d8=$`aYk+=L=+W2+$~Qv@1posYTZ3x#nTTH`C{{NDzay^*uGZd^dI- zK>BT@h42I;IQCaZdJXKqlAMJ+=^U~EMd1x-2I3COt!o(gj@pK<8FXPr8*#(7Z0*(s z)NK&AVSwBH{ew4rqOgCnw6DxGHj2!>Wy;kmy!p^yO2geVSD`JJu++Rec;P&opQxTh zx4sixZ@R)CRs7Kgbcg>lY63pX16j4Jg|$2!70q1T(b0o=&ox*4Bm=-Qh7X3VH$VB$Qp1ntPXJi>d7lcp zlvYU9C+3}jpc#1{qO!F;t-4iy>&Xi>1%i_o-XbUy7ba78}vqoxH&?25|1ypt5S4WSvF zJd56BTM+y0rx2OSKFGiDY`-;ru2;@DOC0ruS74Rq7T6XvbNS|5>v^9751b9QAp62~ zB~(%YY(baI^mLA_@$JGMW+PtTN6kX$N=UtWMET5MxsA5OPKn)2Lq!Ok_yGW~3w4vE z$)0N;R{aWlW@*CQ3IM6VflZ_DY-X@}rSO?4>}*=AC+%)#Ap~vWX1C_M(haPM%)zqi zE|wJpJ{-jL`~iwj7%|wDYeYsx-z4oUZ@j_AZ!{<;Ij6*v^Es&4@Vp*{ho14fvg1GGiSRR|ml zG>F+>MUd>}BJFVaixybjM#N{Sb)4TB{;DtKfBpvq;kYi3^hfmRZ?HaoEB>UGU%SCl zPV3U?u*XTN$Cb}`9#<>!)|VMDV-$w%agaPaHYO+cL&xZ@sRH*%^nXKU_a9IMq zqQD-yjG6>$DitCmKq=04C|A>yJ;j&nG{s1dzcE?~LUHP3+3~#Ng|6mJknr-1G}Ht| z$;t_f6pxQv8m-E|Lm6`9KAN_(~E={sYNbEt5 zMGWHp#1JCq&NE1ZC{ z$fIv#f}{fzxi#H9NZ=C%ggT?nhn@D)tYSuysM;-7J1o`!jCifM$5GT&5hyH~mOlvb zAd*ad3rXp7r6e!vPya__G_xRLeJ z6g>8(=d{36Z~~e?1wEA%bpy~u5mNoMuML+E(MPyI!B6GwU0}v5z#hvrBYuG|DxuL( zm=@Sft_?q*TvK*?9nHIKr!wB4WVUGhZ`+co_2^vfN6W3^g%+{bN9s$@?vG?dIwVn1 z>StYFRNyV#F$y7d`!nHrylwz+Ck9!{Nof*z!X{QFBcw@jNgzz}!6z*~9s`h?2J@mx zRmAAzv0e8;wNjiUh_-La~U>fH

LJ@eM`ouILJ)uv5{RayjyFuyY1G0>XH1I5uf~NCfW7z zQDN#VfW5g3skbq2s-zA}X{Gz-s}~jRW}w(~QNw#Yg6Z>KOIQw)Ur3+NJ4~Gj0Zl2b zSoH|am4G272h-^Oyq*k(02rLYNIFbJmP^6S2DW7lv!*qTt}z3%kpk3HGw5yB zA1<~0hSsxL!gt?i~t3BzVn z&7<#%M$1=so@aB{jsTy;Mh?FKX||4;O$ld_k)m2p8q-VL3k>iIZ?XH7`F@E}-7hwm zkss0?dY=%cR%!b@8AY6ff}}B89+mkqXV;$*c)Ht|oKD0O13j65K}R3OF9*ZT6+#aD zXqZt%*=8UnL9Qj>3^8>f%sdvC1#Ei~sHWNioE%anvM@}v;wwz<{u6D-L~rT*ykx!A zjq`1UOOvv}r1rP{9flahGXOg8{~?VI7jHt2klyU2Jpsb*{jCrf3;SU6Jl_nk&(}ZIx70BVPN3=d3L!I8S8$v9L)8Uu; z$#xpy+QN)!9Z#LvVZts9cmZaKhL_tPruNQ$m1}0eOfX-3g=M)I4;5z3qq<73=o15A zQLUSL2=f=tUL5{#m%6%Xu}ViKAFXb^c$~}QsJA2wD!R@w?$Vs{bbZ2vB7@eg@-4BF zr}ZVe+oAq76=DZOrkd7R;&LcHWjLd!@8~(#xn&Ot+jAtj-*7o~A?Z1>O9w^rZdx9? zmMW}bS1KfxxsR4tmsKL{$B6UCSq(1_B|zowVI6$((d#W{i5llKEgn4V^x`y$?ay0i z>95oA)ElKzZ0lpnQ#zx5hk`an27-N0D)nnHK5ifoitN~FTyaQ-fY1y)y#Pwv^&Cya zlTPG`=j@rVA&$u~DaP5u3wI3c2#y~^Opkee-i~f(0noD;RYjAS0HX0a9o>h+<|$_sY9q@3bcXev6~NcNgwy8DY@2?`ZZWBj#ERJfv% zM6Oj8|bot9iF$lG1MT*u$N8l2cB_UqI6-YNF| zBQ|{onjT{gdmKlB<?PpiAGh6{=TBMl(t68SAFC3PXpxNNJcy>x3RyVtpQw!0yt2qrVR@i+^K2t117yH=FZ{JA64NA@7vR`vd6$PFbE_<4TJ;bM)xp_9@IpbSj|o-6HwT*R)Uz*?1)0@GK1dr|I^b*Qk! zQixBUl)#^_qB%;9HP=RmTH$A!HGjEjK{#aViHuwM#l)5a$36Y+3%L zDO}}#bV$^m4M_8lD(orK<(DNhVGy-BpL9PoA(EuO6jrSJ&!tg(#zAB@G#$tJ&-9{P z5a(+zAgZasg-pZk9(9IGJ_MxU8-IC|>L2s?ST3j?Z+#CN#Bf*_GN#aEas#M>AqegE zzJWsWzcoU{;qoN?|9~iR8q6+dr8sXd_PQ9=4BHq zw>SpIKOQ%^T`+k!8OsI;qSB=xmY%W;pv9BPKZ(dd>U-=C*{+Y5Piu-r^@wbWj+EPs zd=!FOK3$3$&uUgDDvLxwVFwCgt(^;Av1u89{wXgt47$e_j`lZ=Y)9}X3TdBWkCqXd zI+2TzNkbanUUDJR^HuM2D1lABFdYI69|7`iq zQ|LUZB422Pt%nQ`xM4e9)?sBi3z{ZRc9rkX!n}C!tf)bs_uMX&lHLbbh98^`hS-3l z<4e$lo^LHbSLeDt*`J~4p_93_0TR%*K#+DE)HLsw{Q=b?`<3#{djr%@DSfl2tsk{8 zXtF7d7DBZ(&lD(0?8(E^{iXcXyu!Yq*S4T7IEyOgTY%gjmj(|WhGslscV!+*0y&Rc zWw9=5)$m$BJMj>^4(YSZ-Xl1n*>z_@e{GB)ZMuwZUr01>7n$BW+MOsaxD zWiU9D%$bBbzG$GN^kP0@tAp+I_zk{rJI$+hh6(apOEr7rHXg<42}@wQfc}v9to;PF z>UaeI@)6YZZ~i@Qwx#Ic&iFYK-Qn9O)sZo=hKLR^aQ+W!3!{^mzSZgmItIFW-e*Xz zfSx&fP`(&j*ukEHt_YF1<9YB&X}A#SaWs(P+;}up+Yk-iH~&j;0Cb|`tGeYv9H zEt1@c*x^Gpi2fY0oAOrgzkvMo=7FaSqaF9=t`B(+EYn!-!4L_8#6adj~h|1V5v3Z^BMurc`OFT z>bSkqel(q1RGijy3Y@gns+|fUV;f2(*fZh<`Za7pYna-f%-KlN+X3|jgE!VegQKLQ zM*){^S^Mec`B~9SCPM=DjeqH)_K-`Hl_8shf%cW8_!yqC*c#9;v#>D7r{(|xkZA8I z-4zPc?nI%1K)T#tVDi|DUBbQ+tB>a;rWZ>O5tI@E5L`eox@F@Pq)Y?3muawc2_1>gN>PH{(H}Y#nE*GiQes|w z&1cbquE&xDDjAmZ!B0~z=Fr{EJcoNw_f~bf#O1GmLm{E-_yX@m+rbAD@#>L%uPC3m z1cER~Wz}3|Qv%=5ZMuV6Ej~-ho+$!`n2)TiA&R&~+=`I#Nzr?+J4x1+RSn!FWrllj zLMlF-ws_D9kX?GyDtu!4wF`AAJ0bds;6Uw|cD9xt1i5Gy^y2UU1i9nH6tu;Y-68bT zCCMQSA=NI+0^aEfzMhd1wi$0i;;h>XDxrXTVMB@vWTLsyB^(dTQIU@^QFM#RR&`bk zmhYh!8>A(5F}Jd)@3R#T>R6#{&D_?J#oxlLbcmA5WF_DyDX1GswIt3N*D0^iK)Fx$ zvSz|7$(?{hEDH(N3E0Du&9k+;J4Qoy3%`ux14s>xXuL*(v6NWzU~_2~s=cJ=0WHBa zNfmRt$|Zgy#9O>8twbe?iT1<{PbN!P=wDbpc7Dut$b>@G- zEQe7vNx)SXWgxcpNn>gxv_5o}A><)$;gV$j;b(`UA05AW1~r@~P{a8|ja6;>+L?Jh-Kso^IM#7aP_nj~lUiP< zD&wB0`)kf{{nxs)NBe7bx98pcHp~2CJ*>pL1gYNFjVd@(4!K|)oz~aBU?0tmO4rrl zp70KY9XT@yvCjp+aF0oPS;2WUk7>w4bA%F79x-Av)Ky&z0#IO5Gr;qpA#O{*CHS=Y zJPUsK;^>nL@3k0(tzY*;g?x&REm-y2Xa>wub=rP#H|f~NO+qMkxP5=b#knF!p#W!> zt~q!_iP4wY!X=!@;{&)6}4L+3xmOgt*OxDgz1-NT#l zb}+k6N8DXK8On)e8L9kX2Vf5z@}uDD?z98lK)9UZ?Z91(-)s}iGI%68Q#3CT6i|_- z<<2Ac+wjc`axF(PvgN7+a|+U5dh#RTnM{~+9oa(~=4WZNceXe1b1}Agi`6B?(lT9Y z^+PV5YFe~Wbhi!vdN*okpIt!pT`}x?LO(l5<`MaYZ82t>1V`c}A*zmb z`k+}5mFRB~oc@w(s?Tsa*tPR;e$7T!6OLhF5@g&@cFvC?)w!}$lh+CBQTwWNd>&?~|6uaBZ=w~4A zt;Ou}6ohTsS{3o&DXaR5EEOZ$v+Fz8ClXjmO%_=+Bz%E#q#n=-!7%Q*?zg+DQVkt$ z%o`c5+2DgO2Xn)Ig*c6X>)dGsT+ zhfor|jjO=m5>N7!T5fx~43*>OA+-I!Z(LafF3!S8W10PsgqZi5qdVy64Ai&JL~6)P zbtyD+zU*DcR%8_?^9x%>1!k_DzuiFV{PBB>f|Bd=mLK6Bbshtplg#-dN!+`8~|{ya)g1kD`{ z&RK1>IQWhZ{j_ZOn&e;!LsS?lzdw5K`gdFL;=&ly2amYS_s#qff)jco%=ifk0vPZ@ z*~@X?G(9!H8L#krBi^=H8!1+l(5mK_P+^P=yTU_xa@sZJf-N>Uv=m5gU@}%CpOwfe z*YnbLijmu}&gKta8zatHTVy(1?lx=Wm&5t6mjo|hNM|KrRx_v(oj1CHpP87LRgq9+ zynV{;o4Id8O25t>{S(F9QWRe19P#NfcD#|kcZDtAOmjrqFa&!|&!$~5kG&*87=71jlXlE)gt z39SrCdW4w!%5lP+z1uyj=|_SQB;ZIt%Q!^j*dmD;I44DB6eutB9T4GNek@os3FI1f zzZHeU7eD%)x%u>*F5X(C_!smbhY|CiVpJbA!$10VL0mY}zV*a-pl@{*E1;HNt4YfDqOHy(I`+ZlogWA-!H2{(Y;qECL^D~SKl zBVcPz10QuD1ww&IAvN_YN0eU4^*qe!HS-%n+UM5)wSP0J)VY0w4ZVv*% zqx-uMr-?w#e4%URG2%~X;#b%o-+wxSDIn*dA;LAIQGN^Zd3Ocqt$aXDW0_sO zNOJrK7E#!n17OaDqNq^-Xhv>`lF!c)(8Hgy`eE|RX++#k@LW-S_aHqB`DH!P0 z0o}>}QiE`95q5s0o$_YZ(-N1No)(_TTYKc3CbalKsG6~{fg|CN)(E;pof_KGf;-p) zp?$y#ZNZJlXzfS~nyNoh_&rh)%UI!7RQV^wGUI}w^Gx=;*f7pY(wX*xi8rYK`%`@sJ-+vYEfTA?T+bf1|gpR zI(c7d}K#!H%B)=`t(4n^ULiL1iq&d5CyyABb~|$yLH3rcGMUbClgzk z@fxn^@qUU|LWzJ~#t#Blx|;hvZ`sHniY!y=lC#AN7RdY|P;(rb8s$xK1N-A6n%F(W z2|#%HL@pYTK|Fw}&iU_usRTJg%vuHR_N*ss9=}1spC-QRV_K~OE!{5Ny80Jejz^VU z!IHCrtcY1I4jZ~Te9c^gJ9{2(2OR?aOnR@G8W3ivLLdUq5lFyYFhNlZ8^|p+gMb0W z6uffA`63`zEa*CZ_XWZi;=0-R&@B+fkQ@gfwK}25G8Xg=02dMZ25n$njUNdge<1Of z|8Ft)b~U{ZPg_p70uowm}E#8#)6*RDQc#H|c_ z&1Z2wGpq5gNVweX&bei=7;e%n2eNeT&&9>_R&^UlcP^7(p0m|=SG8{}X5Wd- zQr}uKZKSjN8+~Unk4Sy%0ICxAfl=%P!aNCsS-RQYf_Wg;TnQ*=eSUq@Y0F?upnL!y zdslH2;V-I7k0B!L?o9$^YgpkUyNK}2s&2kew;I-POwYa+uzRkrv-E<8_A*zcSiw0>JFvOm6_MGig57`C#z~yWHf# zSQJ8-e_SK-`_^EOU2V$n-BKDz^N`RfMT=lmJHAQ0gc_mIX+lUColz(V;d8A2rGo;V zYD>50r*TV&Y?JQ}M-)=dakHp@)@}1?3@$--oK8)-LZq~&<_S`tt}$e$`Q7{yrb`lr ztpbu)$2NQYw)IDtzm#%RbDnaz)3($7QlYv{W_P_+k^ODIdSq61?@I{5Sy3Jv<)YDYv5M4}LsNb~>=``;IHH8eSUgtYbaOlOFe-^V;jbR<$%TkTO3!Rx&R+Qy6fk|NzJ&m#Jt6b>YtQOxP>RrUxNs_4BG0UL zxGd8%%UUT_s&O_Qcr*uY?pbeJ^yrpEC%-3SqebiBgh^^+CpM(Q#B7#UofYmb9Bixd z2umTyK|jU3qO)Y*I^9(sZwyD=Srg}5J~&FVus~kwWM&bYm$TDmqnknX^-YNj%JN2x zGMR^&8p}KuZ~4`%Nf~Sy9~-c>GV^YKa9Cj}P*b(e#~S6V{H0{JCP&EI+ESzFs5UA0*)TKWY+M0$sS(!^qC27I$7z&pj|bq>CB z5b2V-@$evZ|J^b;V4JnOUhd27iVj83^OBR|nJ}5ZLKH;!NDZT8${y=hCCs+&rQX(P z5>;GAHwMf+6~Y$GI{Rq?4Cpb$WSG;$_mCLkPO6zZqzPT_C=j70qT-UJX~#Zl#(o*c zE=+)a^e_3>-^FH`q*9*s__s0=ai!e5;lva@$v zV#YxBnRRoyx@>oWX7{g9mYTBEskTdM*YX`yo(OKdDP*zF>P2jaV)c7oQcW1J?SE^) z-;y7Fpnv<#u~bFKeBqrB)}K?#!I}NVvl|sosjQbWEbe(EkAKmo$pQ&1>sMLFB~9fu z4MI}Z%YLThlDi8FcE7vwcuS1N=CrE`pRw>sxb@AvJx%B(=*zggXNB`>o~E4oF=l?u|BG1xes#|`fX;wo3{;l&g8a_d$gVl%n1vjNAvA|U4m`v zkBW)}Ld7XG;XdSj4)}RDJbN4D+3SHy`%d=h)2H^I!s*{;bWmrVIjLJJ=)3qBL9BP2 z(&OD?u9IDFdB^TU5nC*L{I;Puy)U!5#|~86IzdOx%n4J-qT9dfw=?0lB_uQJ1%PEG z(z&z~yH;m0Is^SaD-&%iHrV2x<%V!G{dsHwuj#}B)Gj%u3Zdo&s6apuN>Z8G5~m=7 z_L7^cMJlb2AO>r0nc@-UkOu}o*|%JdS^4Pq>1wg#V6WUXSa(a`JIy9}_Ia!KTY@sO z3~iAE36;XxVB-zZnzgd!z6zk9&(|VyTa;+&o*;~3i6ZTQ1%z1zOc#N)8m{} zmMd}_rkF?8LOO$HjJlSlk0q=HnI%ArMJ&iUn>|0I;5*t=tCwpuCT#vb+QfZGgrd3b zd@*MOg_jM|7@}b?0$m}w`a>%mTPq(OJ08rw`OL;Q#mrQeOMe3;p(vH14!tU#`xH>e zfyN3nvpak%Fw;zAw|N@0@rbxPw5>c*y0x{oI0rR6_l?gV9JQ*!McTX1SFc`;9mP&A z%QK$td$#DX)LD^s?ag<(1J+vesgBQ}oSk}p(%6XZ@YDchZtts7YgT-q(R+`=;Yw&z zyGSZ*gbsINu46rfhII~Uk4-nVT|`=e@OR2)5!pCNsF%&!*#zdiGV|@5isdX3K7RNr zsU^y!20}Ig-DzYF{|-aEBauQ>eAN=J_ub7S2r*^A?cPQIhpuyYtTG~ZOFc--x3hqIWU^!?(T^A zd=;QC$##`rxn!Y~oNp#OLaB;9ji(MY()pa7SJ{Z4Y{%BpG_}RBsd53R>E-P>p;3;{ zr}6&lH+A_0&S*2t?MIs+wR{54-Fc2;7g=Vgw3ZexH*^?VG4)$j&mW{=`8hO} zDJpjF6M-i=51l904m^!O439#OrE+*HaGN=a4Jk1}i9!S~45I&+bUBe$LZsU^-uFHr zgkfEPDYLp*AI@}oT!#Ttf7fbb_HM{3Hx4qb1sS4do z)v4UT71u#?HVz(y>ls6K`}cnNZ$U)zor3CkE{rnXy zF?}V6kMk{Y6Rs1+w`&M|wK*Y6x5{WE8e^J&sW}-Pd1(&pP zVm~7Mugdb!iJ;2JTWg;Ju=bJyIlZq{yQXjpeYf#s%61(a-^rfzC2j*Fxa?K79xhec zUbuV;tS6i5gkD1$yEjF?@cY8#=&`0(RaYJ}tV?82IWa<7HXqRa$C%ag4I7W7IvLVP zOiowY?0o|H5eXOmE4VWP&z7CrVsNg(X6C^~;M<@kfD8>Vbdato3^yU5F3p4X3Oq>)H5*M z3|xK!-l%yG^<#vhCmRhR1ZX61Q_ge4)2bu>9*dujOOhHvZoG6Xic78d?T56~hP}Uz zI8FF#6|H2XQ^5)<_rNtZ`cy9ul^4B$cZUq^@S`4kA0wB@M0hys>SG{_v$YUjrQ*z| zB}(GN4{-13=Tw{hpGBWkQH#{1g`Z7Df0n$#9DY_5sCuRA_kqs6_W@4{TU$5z6A+go z?h?#QYDE+`Gx0^BZ#Lc^)fU+sfD-y-YML@P839s9xTU!yKZM{Fu>kH4Y>6}N&wR#J zg2T;fzr_m0?IOYe2=tDClSt1+`F)^Up#w-kpVaO=OGg4}2M=MD^oX4p;T3AY*|YOK zxzA`M(>X&}n{KN@xUK|9Bgg=(?Kv7l{^{b`8-Og>MS48MJiZDXy51- zm3_9y4JGhR5Tc|_@wY?Zclike5H34xQ5!+{fWWHMLs=}=sP~Dv@d(2nVF4>}QtX^A!1M&a--SMD6B85& zyPS_e675!zJB&1YKu1hT%kyLuK>*KXL#RwOm`9#z%$wRq2im+R3IM_nLmkdC6N&k3#``!-lmlGi*5V}tpHemoUC`)x*@7+DJ z9|piut$(W8jh-htHuTA;7|tX%*h?r~zvO5H7G7ZolV!IwYW4r|c4cF^J>_Fzk-S@k z0A#-aAS;dX|7Y>Gz@sOS8pcbyyFtf$9@GMNbcsD%k)m@7!fHji)~{kaO-~${Sz9`A zfXo%S783D|eaH6Tw;#()G#K!u&4PM;wni$@0zg%vp|pG__!HFCeI!?4cWR`#o;3+cEy0hCJodJ-Ig`aX}1s zreiroi$K+}-^4`zN~Op1;rS1EOGU94qtH z*L1fG{qN+DwKhUBFp0>DGmv4q zflpy-ElpMQvwGGcKlpR+;S5a)WF-8rDSqR-QD$n?FFhzI9;f@aWmq6ybX1wo z)n{PW!}&=akYhUzwxh!8DsOB>O*NZztnwO!?MwM~TBp>IwrL$t_Ex8Blfz5nJb~aeMG}qYUPQvj| zVV(dB%fJNvYJb*6956^8gnOAgU~ABqVuzd9V?)5)Kq-L&Xg}4Br;P=}sqs)&-Xa6P zl`fD;4hd>lsLtYQMYa$l`UeaYN!3)G=yE66(Vu^txV0kRlk?DQzH z9(5Uea}v%DoArrXTJ-*!>=^@O4~!$s_>mx;|MUmwOmc_7r2XFi#^|5EkH4+J8>FAe z`O=N^c`ebEk14;A${=(b9uIF1)QzG5p-2ePN5XYe@GdVt!% zY`y4Hw^Y?ttGjx&x}?=8@S?NNBti1B;s~&~u%AACLXeaYQT+4?ocPlxFg0kXkCKw~ z#Iq_AUiiEU#H{p>tcQLk5j|o5^iusKI56Q~T#m5+&5A@rYRF#`zPKp2?!uTP(ZK1qsvQ*r@2)&8QVEIJSR z?)I5HD`C>T;&bp1h2r2~A8o9@bw@|#Q?>bEZB=deo_eQFpTNhTxsu{;7m-5HM^6)+ z4`c3}j2C%S5M1D{59>*+rsE#0&ikq6oaCzbvoxQB|Bz-vQ~%3J9t>Kk2>8Rs1|uqj zA`gKM_OB=S&%gediT-;jx;Lz_5JDuZuLhdpzde8s#x7Kg_^F`_B$-eWf4`N% z{AUYri8a0e|6T3JRc1G`Ja}$*=Dgp3R0$yr~AIbf4Ji- z{4W`af0zHEdWz?tKmh**2!{V3Tw)BUI?CMt2GT$7{6K=>{}vL!|I?IzK=B{${+~bs z_$L(q?y3I~B>rFf{}tB%Q6zwW@3a5INc?~O|6d2i|0xpsOG`@_@rnOGT~?govvRT0 zHwg)eA{OL&!+~%`=E=c-HoW&=aD<;1~6p zW6wHfPvqAx7_PtlWIMnZISw;&MElNmQuF07d=4G z{~~nt`Kf6Ka(Bq$3o;k6Wu${8uJ?3!Rol;t%0pJAJ+Lt(RQ4>mllbDk1JC9lx~EB16# ze=`YK0S}(GE-mJ3e5((+Re;MPZOQj<7uhKlK}5MT*K!4gS9EfWo+!ugGj_?yx@#<6 z-M#a3-Md%$2hA|(1%+M=9PKOLEAQgH8O0`f$pVa)d#`@KTjUB=FEi<^ZM8OFYXI)- zwtUqGS2d%sLkSc$ssN#Py5#SX@qRc5c3Xlja7a8sPT-*}Oh~X<2snD0`Qqk8iYB1@ z#_Cm`;WIp+J5fmI^U3&#Xh9b$CG?K-wKt}9)hoWCr4imT(wR<8jTofj?^6!Ck~oQv zny|tZNN6y06N3WVh}ZqbqV`-JTT;sxg0b=QanxQ-4g3Cl?RRM8^uD;u-?;dIe${`Q zHUM<*9CR>TdM&rUj%piyBelevhFrKD#uJY$Q`s% z!N^p|C&(W)#Lw68wB>ZNZE;lro4(@f3aQb<+L6+K`wn-CxFU%eqJWm_$AXJ@t=Z88Hb93M z1bOvUvI}AxD^!l@UC0h|7mIq#=Wv9zw6Pi;(l>h&J+{Ed=xeC+H(R(LHKjd1L+a|g zv?s4B=pE6USfzBs!u!<{CFHJj5v`G?u0x7C9wVp$KA{oOdWKJs!|n>lGMO%2yVS59 z(FS`HEALIj?R{AY@eQA!Ug%4%2Y}I!5}mGYx`^!kzUePJ2lxN5Vt_z!vTYBC`qb=! zRtg`9O2t^)-qGcG1DTVUSngFs4S?!y&vo6N{M-tC&>pk%k$NYBLeecu>u3jLRh zxt~zs)B;bX@b^Ew;+BfQ6qBUHjekN6Lwiq7PO|VeWrvC=a(o0mn@|#=zQ7bUSNft7 zR8k_L5`(&?XuBzwI25pXAY^(L;p?@PaXZU?Rkrb0`ushb3S8p7pz>=Kh_7IuEh1y0 zqbuDXtc;9}l|Kh5L43HFD;{}M8dAK8jEqd7LWf^OTs-9h8eLU=bbP2kYgRa)U<7T! zazbsz&+9&2PZReUvc-AkYF+q*Agkfk!ee@}$#vNjZS%=?L{VM~-~^fquFbC1FJs4ZIc7{)3N)w(gFbRZu-vl)@`|SE~heS?`=DnF|G|2E(g-34QbmixOD71 zul#(tW>jA@N@o2rdSFL-xH^4#87_`K>+2nz&Si~mQ~@ipT-Zc-5vqWWD1!aW(*{=W z%rvH$Vb5CyNBeu6gM+t?{dbQJD~*v2tYf|IgX~MYfgMj`FG5_+1Vlo$8CIeCej8e9wV0G z>vHaa?pQKA3JQt>@)I*we0sW7yOylhkM$T}jXb>Jp2O@RAu#CiJ(8>nw6fa2uzt?&`EI+ElrZ7V7hSq0!RvOLun5 z$+yavZWsKqEvE}bLTTE7Gkv{(E;iIk0gaRbmrxlR?8MmmBAg)9E@Nd|pO=q_ zc#X@rl_A0@AwDxua3`}Ay3)+I6T_)y3eZOa8F_;9<#BIq3gEkHB~i&6hL~UkI6NjH zI;`c;_*2l25xYxNtn?YLR7=?esUy3_*Gq?3mM~)KwRRuG7%-h z|1x8fS}e2nhe4-;)f%`B`a&YHI-EiYD>O@gOZCK`L*`bVwlS$oJ@jf=_7d_Ek0iZYTHY0JXict-C3Q>^WAo(u zHjVcVMx4o8z50r1jibmI1t9f)B)uL5e>F{a$z$*hwnl^fleg{jL*5pAE-X7dmJ?3ljAMcJVw>Pb_aXS-gZ*nmm23Vls(R%fo5R$d>#wdxn8}}K;1vff z*6dMfEX%ch_M7UAH^(IjKQH*w6D6opG#YEuB5(X4&gp=@f4h7@0&Y_mMjfdSRa0>l zlstlenC=+~sL^^{unF(-C#ZHU2xuI4SJcyKS446~Biy**TXIskw0)k-+1@sCiCs2> z*TL?Kqv9&q4nTk4r}2W#3(10#IJ%r!4DM72`D4jdlx%FAOUAnL zt%?S0^gYq{xkm^QQT(?xH{0K0PEJl!$Jqs;YFKbDdf8A!(;9Y6+Q@z zzp6B!S5Kw!rDbBCiGmLk&HUWfr%2nRL}gB?bIH6YZ1=)`yWTKLrqfEL=5*v0(@B)z z2X~_Q{W*h=iUIul08o&wSGrFV?L29|o9FH63|^bto!A10jL0_!+_$$B)y;9*X2Z#y z5*wY2cxX1<^9)bt0F>IOc-g*%GWn~!gKa){SB%VBH*+p0Tt7LlbEQUtjZBtiQuWno zq|^RV96;~cxHM~Htk5x8+wSp44__1{Vrji)j?IPdFR-M$GT4rU~*d!mE z4vwCm}6?~>-1G_dc|vy zB=FqT_ISLuaM|Uot1s>(=m7$~nQ0Z^Qp1VDt2~sBsxZ7*luJ*>&GG4bA%X5jU`#Z3 zRw2J~a=vt4y`jR~tBK&OO|4xoy1P5hU^rwXB?U!(&4Rvx&wQML8jIoGDVMOauHR8t z{IV2}=sK{t6(&0en!WC_Y`qvz=AXJy2AM&>pi2CGugce#pIcsxl8!Ujgn0>ifGbWx%)?7g}! zNldqNFJ$`F;V&zYyc|c0OpVeS))#rc(~rhCO-u~w6ps2B1(C#WpiCgs03~#0`%^!8xU6ZF^lT-V8{%73PZ_DnrnBt;Eh z#TId{gKGfsT%j9xr>4uDZSuW&U0$9~>N{My)7TE-@HSz^gNA#MEZG1t3y z1mL+WC04Av9)iz0RyW*DX-0rgJ{2ThEa{$mKs@n(oQe=7160OBb*9aCyMCO(yk zZ8b-={r!VaP`9=#x!#cJ8Xf9x4*z@O#q!BoaO>C<9rbJvBgxc22{|`A zYi!)08_;gd%zj@VuN*wB)VzSl^F0LL)f10&DhpoE^MeSx27sCEk;-krweAMe*J3$W zf4v@OJ)EXSRQ}!2xrk?FS5-_3{?2>3J{smCf$nvZIPr-2HkGLn+BNfh(ByV4yh6^& z3x{K1AD?PZ5cO40KM;MG@P!dx=Yuowh z)Dasn-fj&_4QuV~BMSjsL`V+f`TotQeAFVn=c8;L-w zx2UJj00KBOI=)1-fhfwGF4@R?c`jx)K?6Jf0#sW`9BG#s0TrT-lJRF+7z~=|!kqj) zIJOa&ql1wd1WXdk%}|~b0+3|mI6+X=xRdjfqu?u7=NT=`>ob&<=a7z&5RhSHuWIl1 z)htmo-C~2$KX^1RSaQqa@rivbD_7OwC4^0{fT8He(wMG8{9~5ax=$D>6lAD<_!jEy z7~Je~4>mTDjt3J*Krz=_gnUzvMSEGs#hX1W$$>n}`nwggRr`{@u3nQXIrWH?Xo0HM z(#IfRF!)gcjo$}dNn&i87*&h#v*QU}yW9iYkdY4y0gY59U=$~4IZSh>RjOa_nTS&l znDS8$2P3`uZn#Y9`R+9O>Wbp^ddImdvhe+7e2v*g511|gn&ULhYMUEB0&g-jor(jD z546|fvsq7Jw#niCIk!xa2p}f2ogPy3Z_`vYJZ79>0+#G{Mf!7OQp#DPy-<_qgE*yH zM&FgmxF`>3v$b-v=X)X`@Lq=d`Y@Dh-w3}mqeE`Mn$^VcrHhWud=3-b zFWedU&gWg@SvcaF*-YjrUX4`EfGxt?M@AgOy zuEPTamOrK$q98MU)fZ4fMpDi^-lhj(VC*VnvJ93qj->;Z8pq9ff{Z!#Bz&1jCDq}S zV`aeVa6FIq_Us@o?H415i+07FwOFT50n*_h_?ZX4^NT}h0_`~m$S9AL3~H@JCi_Lz zbd>lofbLP{O6=u&qd4%099jfu_4O}wnVV(=Xzq{ocMF=5h{8t}`}}rt@-GdWaFVpB ziZAIk_T&M@qBPz_TVJiF5M@c7xKC0GpwD!?z(>ga=!>N}$h0cW;j+@MftbnT!z_En zQ#SaOqy18TD7Y!YR;oUCxzY9u%qy`8;;iMNbN-2y$@1zKdA(V{H(!4s%UEXfAcInW z|Fr`pM*hp%%k2OSm4k3DRYh;ak^^suxymr%>+PJ#D~C0w*no1E_(f|lPo3sYX;cBR ziiw-u^n+peY{d~U%6wO1UgCt!q({tCXK`3LvKpc5^@#YTxbAz*ppkU?HJT#XuWZeW z80OR5@x{hRIS*FXulL5lezy8ZIX0gKa`J3BrLBFX&3=VitpM8FOMO@Ck)YxF@LgcJ z$`{Y)`<@`r7Sidua^Jf(M!56S*vIv4A*geAPT`Wkrnlki_S|6fm6Y{rquN>qIglB| z;CtJPVpJ+0k&uw(c|gKS*^;97L~Q<@(OPv$IUk!n>!OP9&`^$`Bnv%gKM-? zg9P$TW>4NawTH@alWwFz+7G+VvrzYEq?!;(iJ1_(P^o^E(z-rJc)Xq=nk?R6x;>kW zH6Sy5H+j7%kt_sOwv=u!rXT=W5dMiB>SU`5-Tvzw?^+r%4_ zUei0Z5W*asH*=%YyPB@Fd8@HCVoiPvz*r)5&pEo>iXpbFELrAU>18R_l#yVGZ+XqhXmatu) zg-$g%Fy3FxF~#nRf@yTyeVeNJta}G%6m)G6?+GE1RoBf;HwXQf8gCoQb1wqIfW~sL zehSUn&<6Q#269k{-vVg*AK?etnY1-5VHc@rGFs?B}Kz4R~h})U_&K+aHrcahO zKWRXw#+b?DzTkVm{e|538pMGtjd|HrU>~`a=Smzux;<%F$YhwIm2=DMR`5+WSJl!_ zN1I0v^l-5iD`njq+f?#5VMhI8sS+9K{l;z;GzzFnSge5)oT&B%$c1-0ItvDLGz|Oe zZ^e01!(?=TtW$x7k-*X!+&1^xzSW8hp@15zp4U0vu-2xeFPD2MXp;xCGgXY$9Bz(Q zL839~;_tNQ{iAW3i}vokwkEwC=aSK&H7w_WIYy5lHv_I(YxIZ-B7(n~gzgx4P6bJb z!2m04xP3D;4tPS{KJDesd5ujmb%0Vz4Zm2z99oJaQD{c4A+FbVaWr;XLq@`u>mQ|y zmBL0*e75`FL1yGecD1n&<>CD?nH!?JdS3#iUslw8&9g_NeG`X^X^a=u5x`a8n|IRjWRGRuR~)#n;TIk&89H5t~%Ixs&he@2&vw~Do(u@JS2nR zP}jv)C$M$bYlhXkNo}qwDSNWMeu|ErU{KoSt25kW{!eSRu!*ji1^AvvEWz$`K&eP; zJG#CirrD9es$&cDwfPrObG@~{;JPcU9N!HWq06IJtDq=)sO(vlYH0b)coU4^xVrH+ zGZ7>Y%>iA}ds4mDu7I|f8#kQqDSDCb#P;JiQ(~IpkC=q1_SjRyWqit?*!L`&5arYK z{y<^X{>H&~*&R?g*NT>XPd|_bAo2N$XSm?+^z-{xy3tRvF`!TAI3uRJ?9iTK-LnbJ zS0woY>(5*u$C0^5eGsO@@gf+_2{2If#?W0LILc8In(I((U{fmjQ_(bbB^Di@%cgVI zy`9YAe)y%${UNzcs+BRC7Qzl7{M!KzVoo(3ZZw~vELE%e47|T_s+0p-IJXo2713SZ zxw+&$#CpwyiH0TqWckJ%CerDlaVgFus-Z||vMW2u>598K6^?78*esU@q_>Qut>S< zoW(N3b5&Jk0L^#{Wu*>0e_`kV@pwGle9lecfwa)9Lw|2R{d$_9E;nDre&f{%9w88# zV%75Hw{eHE!5UW*0YXnYJFtDMf5rsL*_bRfl&dX73reWq#Y5tIK&;Q_8ojsB^2SPU zMA`v4;-x=Za4gqe1Wv}vV6&f+x88O=a8VnHaUzjdst!kQ>@ou!!Rl{ICdPxKhV}4~ zgG|BZzgdkc5VWWF_o`>Fe0uZL$Tm+z5}CXN9i_61nhM0CxPyDD+ehg z8L=y_FBfnkuY~|**RP+&sXwQ8@f(d4p2Obul{uaa_VOyTF$<)w&&AX*&6b{pR^uSS zj4pPF-m_?hv{<*pBkbfvxgRR2*JA{=yuW}m@Z@AT=*?ALzEkcLzj*ah#RzsuB#}U4 zr5D_z{k@*TySk*!$UK2_&eEBamQH?zYF}Kz%<%Ledaeb28$}hh64JwPS@7)kE4|FtZ1~PS z_dIXUr%?!Sl{pEh613G5(qh8PQhrYD%tG13=i48}u=?Sq1g`Ke6d&$*$VWbT>EPMM6Y zwF{GNpP6T_yWc{(vmFl{tv5ll6)zRhGEZ4Rp+*~}MtymNI}t&E4*iKFKhB?Hx%H5P zcJ1Kz)_XSDvn36kYNgp?Tz}J!44*pp@ox)m3U;uH__(T4-v&rHx1NE}*fw?&Z01KJ z1yrJ*n29!TEA8bR!=7JI?(KQTM~Th`LOq5<(G+5?$BS8H#i|L-TQG)hc$`+f$?U%Y z`Hvm6TFaIBUK%AtT@32jT5<>#0fr3M>mjN!op2t<)kk8%QVaBBC7I6(e)&}0lodw5a!26LbJJIb{?nGK(QbX@ze))DHBcsj~hsl z+ct3PHG6Ok_CPEK^~3cd90o#H3O8rqB4{Lc4BLKBCh=gI?FtvD6d=nd+1B3Yj0e&{w^?>qeO8b0VwPt&wX5C8wosj4S zsrI*8h-9W*g2^G%F1r>f*%N{rhq#Lp9j;1*=o`nI&47$*rotGuQ3QTCVl~^%=oRCR zpAL)EQeKskOi>CZR}W36+QFNO*_?%H3Uu7~CEIbTVWXu^bMG&=_P-w8vkWcJ!@6{#Nyt~Mi+lY{S07Vvu3 z=Vjy^F8;c1fkDpYc9<{M78UiZ3pHJF>a6O=Z3#$3sNjhF#QDemv<(RmNh4lYjz&hh z8oI+4aFgGNAe{>v3gMt=) z(8QtpTO78_wr&68V6;pPJ8o@%WevkXiSI>E{ugVZvrFH^=N))0=XICc>?@LB&+=0+ z^SLrcknNmuqh-J1s>2ijO)k=xsUiXu|IUsDw>&ErLV2gqBFVdYHAtqjsPa|^UnfQw zVYS9H*zco6o@Yu{uYz`aHU%y>{)D#@&0@4q*-fo#!Y0YmjaceB0cunQxg@Lp8V1|_ z5rL1ZgE59z@ZBlJ<>{t>Mpxp#x(b+(B_|I=?I+a2w_U1niqfy3g?^3m|M8=0Y$Dy>D4IYsVJuWx@B>MSd^BjL~z2AhkSoaKJ7J`5Y z+N8ZZE%jX_5xxOrjJOkHc##ArC`8(SjN%~Vj8@6+A|O5*#d$}5Wya=sR;1GceadIx z@mm?#@OQd1PWoaSIm?sb68{?1OkRWhMU-S512pxKKtw5iU^21}vy+>CIuzOq^Y((X zK8K!a03r0QL#h;&n-g}l(-`ZKHE_D2n}h-q&^ zBs$}oFsUvWYbF2wY$e60iKusYz)gI>6cja1IF}M1Z*u)KEybw+`goPOWw4VZp~$e$ zO-g7Q(F9q>^U(J`_6lx~HQxJ>(f&8gy~lRkIAI$oX2u#GEt;*7`Ca8ORl)VPMHPA# zMP@wM<+uJ8u+v>5+Z`PzWg`TS%uJS-76S&mL=+ph*Xpjgwu`O9P5f=$Tf@RyMjH!U z1hmAL%=2|u`8(<4zF!Q}u`Vmq`LKgJW<9};D&4>!9Aoz@%}k|%C2F#BL{I-y%;fK0 z*T5a%HZ;{P(bJ3(_nrrb>LH%O)8O*P(-6diFSz)6l_nME9Ihg1X2Hi}L*B-o%w8BayGy<+)=LT~m~>C@v4e+jUI-P}<}uf4ZZGo^4Xh!x z`vDgj5y~gUJ6QvaVg%|e$@Egj!g*;%%+(&Lb}=QbQs>{&{#3{4Tw|c2`F= z^E9yh!mIl_P28+45ocC2zx3$3%*WBFU!`9SufZpBwbp3zCH%J_GKvCuWE6)mmd~@} zah7R>@}fO4TvC+eVj4zp%rD8DA77nZEC!8-gwpEyZ8quEWh2vlCX+2~*ASeys|GSU z1#^4MmU1zd9WQ99s1UwIFjyL?kYMlv#y3Vv!#dK94`SS4;<&bwk+8rlK@jznHBN17hbb4KJN$*1yB+T+;W;bLpOevDgfp@YjbFo1)-0C#GHq~r zsg>5)iP{y*3uHnnka8PFuw@d7kDWbd{3ZH zQENg9zDqo%@MISIt%^o5N$W?APKtL$eDBcIq+U2v$osm^X_s1C4jS zzJxbAQ!}W9vifwh<&NdEq1KJMG3L+4*BK?qnr4lzWUc2%9DOC^9Dh{d zOIrf6JFLaYvsEvESa2$G)+rLRxdU`976VE`&l}#*_8V9Hje76P80VFv#lU=;pHw%9 z@7b@*&e2fNrdyBaRaTW=)6A*;;vYFRV=Y7KyqXyy-5u4FANf1#KfdqDLk#12lh)3~ zlarDn4iGhf0dyJx-#mGe$qdFGNEbuo4dWDE$5I-|w8UC~Sw!Jy2JJW~N zFZB|TQt68Vnkcp+7aW!A{wesQs*MOdWMW{`E0PO06?w_d*} zYS9i{h>hS#E7iJ%9F&SWJTuSef>--f^@BV|Zl4mFV8tint`%Z+NDwpwoDc zR^7o}$@IyY$KA=~?Bu{ZRR?;ICBb>K82UjsDzh*#p1ia5ev?JITZL;v9D^aRCb!e} zfBqOt_C>h+;U7F!TU-yzi3$stNRnx#(wCe9zBG$uw!-`)*IG-g4obvjHNioQ+Fi=i z%v_O!C`-`?O+44T$qFjt9yU;Z!+aGcP)dGzKC(N$uC?Z_NsA&!TkQ|1=x6<|v>vsw zv>9-lqRAFkrI|4vEQAS>O$2c*f8nqU4kxqrHA(t~Ff73Hw}Pni$jNqIBBN(*u$dRg59Wau znLGX$VsiSp9!NXnoYo7wYaKHsM-ltaN?LB$gr#e>%52vMyyomS_&n$(2o(aNV zEY})(#-dn{j9;^b`tV-3pi{ZW^eH_P{(WBc%&V?YWrL4Q0SX3u3>g&apAf;5Un;>U z)x(;Du$g|rTY3B@mF3NXdv>z0K$l**q06`8D3`jk{d!zdau2RtXr9Q~R7!_|`d*Som8tGB3{JS@!8H52&A ze1zWyW8-^)(FoIJ$vpi~t^|v92<}F#`bMz@2L+j!MC!k4UEe;#x9@2HTJC4YSTCG*)$`0jZ91eMzyrRf1h*nXNN$@IHAd}>sosntYT zzu!?7`5o}{LvjC%$nt*U5f+D@#$T+5M+)VKY0(h;(?Gdu6u*d)(>nfR>z*QRB4dN9 zY8cs*b^7NXd8Yc~K%%DYSsqziEN#PRx5*OxNNw58-tHI?fm%oMBf4gR*HGyf$r%DGG5C?uOAK@Hq5ZrpTS9O8 zkbS+bjTNR^@t@Y}^ckE)yapwtI>%9t{#Y@n3H1H$zcern^60{*$bUR``34|UT!a~} znvjXcgG5CJ0`-qnfhc^;b=~Vc(%w8mt#OgRX*m^2949}X`HDF*ueSE&pYbZH)TC8n zZ_*kznDHi>a`z^)gt1voLYE4nNbybzq?1SsCS}-Q*S7mYWCT7|1p#%x7qZgYcgQ?Fk~&zCbm0S*wTzxsyO8!^?GteO3t zrDECe>wdq7&n0sFW?$BJtD>VY`Wmi3*wQKst9&`Z;y5wo(Wcysn#_XAopYQew}5sT zHHujdyW&j(UIErYK3(gIB;s|&An<17oeEDyBq_ZsfNo1DHk$E@H2|qLoeU$3hb7y$ zx1i(uyMl{I1yESE(ZiXyHa^)E+5Pfr2hs`d3rW~)q@;2YKWr$|^?CPLFfD0 z>ZyNzT6T_LC1B%l3H;Bu6-^W(^IGyR*ul4OYpd;o{Rr>aF`ri#9f!3J66XrW!_^8JgXUVo{oH zmSU}VQGf{2UL7h$u@_@pk;CtkRTE6`8~3m}ZabcI>UD^`&K6x48$dYK!0O)t)j@EdVKco2vMCtl9l55|&l8h;Cmn72Q0Ko|0exo><- zcE5ZQUf3K;0Qo|)pf+*R;H+?zbs<#?yb3J6+bx$FP8QhXQRQzEP-6bAm_h$kB0_3- z50H+=G16HtwQG)I@+iO?91qits=U_e;l$Ichl*x1(MO%Uy24eN0=U`69L3E0`r{cj zRc<{T78})sy6tzQpqGw!;j~{)L6EM0vo&*kO_GLYn?@>~C#>imwZpsK9(u5^C-lwM zN>PGx8!hy!@$9cxi&TMCj}u#L`xi17KvVYHN~>o=?*>JRw(0gA7S-n52JcQVod1xU zlkN1AA~S-0SU)S#Q<{wU_uO{B5Cua|2?9!3&9PowyWJa)QH@(pr}JfR*O~U78b%P7 zCGLXraY+D82hGmK_nthL}U<-2eZnh%N-m5d|G{Sos>c1yCpa8u&@`6(PjKlWfu>S(%4 zy!vFyuvN-)k;_>gOd^Lfx6|2jxZqw+L`C~6>N7kag7&VkvjZP|C*1^-h7JShaf23{ z`T56dyIl&5uD~0d7MC45C8)j^;HyaCg-IB0&=g4K(RG3eiji&>WfdtWDuJGY&f4y= z+q;g8#wAip9-dJLrH*Z$_RkjmWClPUt#9`$`@IbLX{o89)*Wy2T1As%DV#_h?;zg! zpy;Jq^PgJ4+g*R>aLHyBJdHa9>YjE)@-?cC$D8)3HtC3XpUqO6Weh&B^h!eLYQty6 zmB+em>_k>=1sBS7qqX|p%ldfxvQg8Vkhp&790nm>V; zF3nU}_^)(V-t0M}D7W11z| zpc9y|V_Rmp#x{b%2U4lT0-3aU-&J34 zj;|gEFt7|BCs~>Ix20?zFy8x#q&VDH#*veZ2Tu1A-w&j>YQS-l(&txw9hsU|LHBDr z(h?;qWuu`I3ZY2X(1D`N(@rA_gb(wT^hoQ&KR=>VxDZ4t6p}>j&FMe~&G5LroZ+RU zss1fR0V$m^9w6|%fv)8>kmF)GN@2Fr$$CxodzDjeJ#f^W4>v(PZ76z=>}SI3F*n!` zmwH|u7q7no7eoOy9U%0yl-})yrxTmCpYMIjSZUtB@)$J#K(Q_G+Z=lw)oOu6Y0Z-`2e%@rlw(C2eEw>mY9$|-q8Wtl*5LRV>-j8#j z6|B#XXSnBGY)}i|l^%z*`NzU&;-WN~&_8Cp`BMgf@ex7={kn(%{~041wdwx6$L9!s zY)+6R5ZdOF>w(pm^9i~nj`-!q_wkqc(vfgr$Izzdq)$b~@^ zzEpR=H2}vtbihlYSXd+~o*vo2VTk&4mg0Ks58~s&%fxnm31jDk?uhD1OnuHtDj(n1;t+U@LXWWqA1P3s-ci>hk~nX{^%6rNDzoY2AJ7nRe3? zHxR&2+Tu{I@}oPWCGCm)dFU&}RNFR_l8NB{sy&j}yCH7#T!h#!BQ%A=iJsK~%3zcu z;I>ff7rSV?pnU+gfDRSB6JrvUcx3Yk-Ite!QBk{>D8!`5izS*LqL$&xJmWevzJ?xu zvm6t^8nbR9?1D9{Lvdf=*Imv{dO43nUv$!aO!K_boQ9O(&9%`wKp-mrGyuV>$&Ibp zHc)PtyV055NqMAli7m)zP<&YB++M5};2#>!zhRTkcxVu@Wg3cNe^55=b#s}3YP=}E zk#AElEKLUSyXjmbpSNw1F5zuEY;jkMA3Yr1*tQLQ!*sHI#+#u8LbZ_u4AiKGI1$?8 zmU_^3AI+O%*H$hCyNXx#B>O*YO*im$;}o|F)+0_rAI;aLoWg8RXMGwezZQc79z}Ha zj_3d@zK*l{l~FH@7P_z>_}V@G2v@f=ayvB>0<2=gIS~TK1YL5{(jsLVb?|9>#)P{A z!Rze{dVKF^?k@X7S6okLoVMqkI)kx0WgW?_unXbtU<=xA`R2Mt3OOETH)5}xo}U@G z+64VGTrs0L>BVGw+8>{HO!r;@{2;>l>L)E(qSR{sv6u26dr_5N&DOWbU+p`Qux+h$ z;c+<#i8{Qfe%Td8G@PL}shNs2`aN4t9&T2s$-n3`>!f*7ZOt9$lwghGO{zV z;u6~B7FY~CgVUsQBwwV~2CyXBUUM(#&%ZhA4Spob6e=s9!dm(Psm!ndu)zdYL@25u z`;jv z`kZy)g|!C>Yv%xUm-Dg;rt8^riIp0}hz&0IG%JA6^xht7OE!A1^C2`nke1L5p`)Ec zmSaym$ExQ1@!_t_cI8aCiqVU1UfSzNeE+a>DY@;t+a4<8Mp>S5S}sX%!2DOQb4NKf`KmQ1YS#Lh~XW^S|BBb#Cza{^LY`oxezQL<3_mon&1I_B5Y zjbG`RCI?^`K^MjoOW@P!r(w2AJ;goNH#bNX(p{%5hKMwL>nJ@3!G`WyEXqrCT^-gk z#w*J7wBCZI>2w~IN0$vT9MGe|R{WPBg@c5z9r4%DR19gUuo>C#9Bl$|o#Bm`#Zo=N zixCurKC{_mAxbj4xxsyte3wx{Dm~QlzRZefRs}#YaKSTB#B%l)eILGd-G;`!S7f^* zA~FaHJyVR=5<5R1U22nm50@$of*6iXV|e9AsU9AO5oN{k4jugwKbC1Owzd7M{;j29 z*jz8mY&H|K-dfAlM9yxHYtt<(InyaMbq0@Emm`D&SW;a7Fh7b#8a0nw^}cCVjBH3( z8&J3UKA;FcG}e1>xnhJ#tjMuBzxS{RYuhBTOjB(}&9iLxyP81f2KDw-Tv++m$Gn+UMTuV0`} zC3pfg?*123ZygZT_Pq~NA{_!s2n>xN-6aD^NF&`{(%l>o1f@Hrq@}wt)MlkJ%%gzRvV=vP}Lbs(wb5_upty97H?7@P6gE#i%{ zsIc}QDn?uFYPYMFIbJB(zb&X;YC0r2kjU2k-Y+PYO8@daU+?^!2EB!&?Sa~v!dmG3 z+qb%nP50XrR(rY3_Jaj>Yrx^iUEJAf(T72^MnAsNlr;q~UpmVf`_R?bFGk{MY53gh z;x{oZGKP4f7fN_R_)Fi)^j(Z`{QINLW|2DChpR^BrBZ=%1f``KI(u@(HL>#UJ^(!@ zcWE<-tl4nSBr8xwB-6d>qdEsUgu^_y5ts}s6hVaWEmoFbcK>Vkpz z*TAXioD7gZ9cYv$uC>^8iaoPC)@&(kDKqQJcr+fQ`=Kpit2sI|OIIZN7+#IZ3uZow z7o9NE*Bq4_QJ0-#W_2*}T^ur=72_FwPoziQz~AEr2BkinSFeI}Xc$Tj8f|79U%bRH zCT}(&o>m0?pi$#bE~ z&6I7r&9fe&do%aoQEC;quN|}D8k!X*+CMF4HJ0k%_aIE{vPOi5yba9+p;L* z)2U!oL%OSP+XM`Bn zZ1V?{%y;j9dJa%)R7`Z+UuYAg5nx6+k?Q+2G@7l8<}yp)Z#cW&*r9b$rlsk=jsJNg zK{c?>C82)TzizOhddk@$c<^5OabI-ds*mv=I3DyH`k@{-5zUFG`Q&)NF+h6Ab-PRY zCswRf6G;8D4R;g&4fgZT^G|z6`{{myq+CVWC*c-p=vJ^!i-YrCHfzZRJzbeDH_g}a z&38QV*luq1$Y*wTpU=W=(#}3Q^+W5{>zL70WFPPi(;nf58*ee(IU;8Y@#)_EJx1Kye*$r9nIr1k9I?k zItP#|_+{qC6wYQ>tRQR~o3zNX;+8vJKhKu2m-+e8evD7voNjhF7+r3V338A5a<(#9@<>iK@&r{9=r<6*an;4L`pdr#49jHPo|NxkxzM05-)+)+@5H*3Y{1{WYhUEeH<3C|6b8ohoou#XMR)iIj*Uz4InWOmvCXCtbn?Ius2yo6hyk z%~NjI_NG0(BC@IG;(!y@xcIq9h7#S<+*N(-5UHD+0@%;qGjyh+fAgRaUB9ivP)m%? zVQ2oS;AuVdVna&qn44=&L~nk0s(!J>+*Y2Q+3iD%$T0`;2#=cmC2zJ;EN_+@TA3Er zGRLp*TWlrwaglshC8X#8(jC_RZeSvnL+-)xr9V%fYJ zpQn_xn7(bNTm6Dnkn7uCWhI^z&%4}6Rn8h|PE6nHt?m)kt+&KXxHM_rc${fbeuQO1 zI@p=#>N&*)En1yk&Sk$NG!+i$C{un>zRGJ1dNrKU`;;mrijFS;*d*e9#9?+%y`*LS zHq7Pf)t^@ER^q8}w!p)>b;4?|mX^~r+`4I=+w)1cJVZ{5HZ?2~F-too9ju z8Pz1S)N5o9!2SV)_t5kE#EcdQ>{mc^0-0N=xKzUz_zTwmyEzD&E!N5+{F5p`JSAy^ zM3zzb-Hcm3c?8|37i6spUW71OuU%{RVN%rd(%TqwjT{lPe)=d^|KH8#ShjKQF#+wP z*fp#Sv%I`m49QyD7ZnLpMzfSwwZJx-#2>xWru>1fh9Ms?5+(fozXonvIO88smK;aA z3OMDsh_`2rl1AONE|2Z0fn09{7*D$p<7*c=vlhvsUAGASNsa zCkE-Iab!6m#Mt-O>Hp(i@)gFWEf!MH^2?Tojrp4=1R1d}fO?C|1Oz8Wt~OhA8tHzq zU-Z}cf)wjZF7JkogpMxi>|Bi$Yd7sJxpb^n9%}X-=bgezM&5jQg{n!v>2V2;XaRco12~O3+&hA@xR$zU|;gcm)%UAlvInT zL9f`UO@m)#Tl{LaXpa{=MBIJKRpG&zOyb6nqIw8(Q_8-%`(~JUGj4FHp&j$8i4YRl zfWYo0n*J6`&#-+cHN_Kb7cFx>(AZz7!_i+k?VCp|c{`>Y_beg+5`wUg)n|8i-nRDk z(Q;#{%)fZ1mC0hL=pb#-&sk*`M>430kDmz(3rp8bWq^=Soly$8wviDf##?TmkPtMS zX={lZRbXTO;qCrXj;5Nc-OhV~5|bae=tz4DSF2kx^J_<5)#sPs1l`Xh{TS)#QOnB8 z5DU`z7b5AlKYRpGq$eX$;54HZ5as924U{z$xN)GyKQH_c>B{Jfr9ORP zd3YO&J(LjZFK#zpIB;I*=h=PTJd#&|C}D1lk^kzemj`e~kU2c0G<#K9+xUO&OZs5@ zl8K+RNQZNXxY}zaEwEXh8wE>G^J8`%dmq_+X%rnl9wJ%jyU~oqVM!*7Z@!;hg*yt^ zo&5SZpCsEeZQq5zMVS&P{+h0@suEV;vDDz*{m5FpeRu8DB6A;KEJv;1FG%d zN9)@cuKu{>phRgM3(~aKbaO-CVv>h_iR%yzMRc5yn|}WiXpZ%kH#b-;QRF{ zI$tiO6vxZrJ~Vn#u3*Xfx#d}ndZfa4Qe?MShK02Ct+&f#F=Ohsuy@R704^p=J^eZD z39*A9fy=Dq#|@Bu0|s{f`zVOb=n0ayQOH-A4Ep4D#|YW?34_xHbw=wqn3vwqsTRMN zn71KVs)BF}ScS1%DZsgCd7Upd|&nshx~_)qNx=RF8X zWfN{L__^Ou$YZ5)HUrs(ma1TJkmip(evp6zqKA%E9B2&R7n&B^#6st1Nss#PV!eRV zav^>5#&oLR!8}5sPHKKdQh}fDBNY_d;;(~@A_P+P^}T|%@I3A#zydL*T+@esR)&r2 ze|2fZ8`$}&IRS`eU{PArW@v_7P-|HrbQBG%ApjV}ByE`O8NN$!E&MJV4^Z!VIBm;e(-UnI*2hV}F?_%tn zY=cbB&Pa8R0u@#>ykByc`RNT6#IWVUIIhcjyKK07tV*Ib_Em(QIl>u}vAr=#6F;yg zE~+OlPx#^+c28Ry!_0=S0-lmk_5GQPXl$=w>IeFN`SD`V;Vc9D%NZDDNI%Ef$Ds4} zWtUU5;^7%@wACu?N;rDjO}Gobl9DxMv_ZJmIWF{)y_=^}+epbd{f}@8Da2>8V!tLq z=>#(*`c}moAy4zTR?AZN}!_LGA@WP{YmIj8aLBLRjgg+2E;qm5Q9u!E(ySPH1C zt8bOb(x;aE?V+YgDxmax|6wKW)3_=E9TA)-*Yq#Fxa^qz&zO?V2VpJO%&S_U7!UyoI|=*_eD(*4j;qBA0hYnz^-@uO72yY*?hs zj{Jph2P`IDF6|*muwW^TWfH(P<1AgRnhk9$#BgbvR4=%05Nh68R(v7y{%{wlo@98S zF>7}mr;Z$lqI%g#Y~fVIeZx?=kAli1t07r{q{I%#?-pvMX}&mmLe@66Ca>|9qnTXZ z9E&zMAY;sEllF~&(09PiEmozs0evfWd`U~=4S<8Q_K-KLfFYMLd)8e%f`ljC%DTqd zv%PIrL0Qh-3yyPjgo@{kE!{Z94wsr4)7h; z;>aX(a#T<{i|k=XEgA|-7o3|I8T|!8zcw(qwrfY{dTuD!@hgWwEbqUXu@~wgybtij z^&%3(IK2DH6KA!(B3WbUkg*V^j39Y-K)_}4#iw?3#N*$C3j~Sjm>ha#_#aFv(it5V4>&E3K53c5iOHD1ejaXR(Or{Sr+>I_kcc$RppJ`nHjI1fd zLYUcTRS>0!Xq)H5J;Rm7-@2keLZL(W8m27k1GwQG8x2lsS~t>-Z{rHhesz<;8FRqf zcO1aly>U|a{kxfe^^emRLj$LRtkK}iaM*D4;hEA&yWa5+-%+FAY=!AsP?_N%GEf4x z+%(^85>cQ4ro4F)lf~M6X=5Lag8>zms{4mng{Q5yZIo5jG4=;LhTmwMso7g+ryKnz z!@wg>gYyCFpN^is{WMj;jo{C1ojLrCx<>T(hYYxPK-|(E!^U4{C0or(GkuflZJQ03 z+)qG%cc0vgo7=sT?n>QUx3#F8yp!HDHOJS2j2@amxe>C-VA3~YK4*N|_s;x6XTyR| z2%*5x`>|Qq%?vniVf(YGXcURh(V)d5h7&(W`%dij+cEDq6fEWZ2we_tNAkvws_xqY zZ?n97S!q6wShHY{n#5&eZ9wX>VAnO`x?k3Pe933C7NaQ3Lh<+QAUZ0lh@>RS=0Kuh z3_U=%$gUG6_~?q$HMn<9fgb-T4>l)MeVG(KT51~otxO*spxgx#O>JizLW`2BMhaY* zDyMRUtSod2b|UDb*7)xbkVOem!Wcox_%i|q z7u;MK@!>0^)Mn*h`ABxNlfD4_OWVs?Q=;t2R8LXk?FmIW4isEGD!QK`-GYR#Y#L7j z)Tb1yz9?tDb=X{A@;K^DG)*-R!KCW}_nY7Qdc~uovy+0Ab&OW$9ztcRl}D+lkO}wn zb;Rp#(M9Y>mE+`ck@fY((em<$LSn;Ks>i@97P>m6BwddK4sdy2Vgdr^SV^2#U(iTy zn{GCf^AvN#A{}PSjj#XuVIFU1h7)cqZ>DU|c!bFco^;?`uSd&4H_{N{O#ShP_|FgK zr==_uvQMC%--%59&CnKH*FsG$ze`f1XP>Nf`|T*2FEiXMG?`*Dw%pr;g+5hzDn&^w z(uehXjOTmP!!fXUw?J1Y(Hh?$gOk-xwI=tfUes|K6{X9gmM8Db4tw=UKTLifA-|VI z@6CGTF$f_8FVV#baR>aIF8lwyze2{L{yV9v%Yg{r?|-vMaoTj+N00xWpXPxXxn|Lu zsJM0&MbX+c#p!i%-6o--UYT{{-C=|4Zhmf-<=RN8zDS{Z1tRctGYl|4uAJcWTINMg z=XezA99#{mIp1e0h-d!12t~|bSs$yANgUlSFcVJ%X^gvh;lwLpe=(hu>3DHG1+_;0 zfDwPl53xT_2`X_?V$QFn?jecTD{dHaC!$KMAXR$Im+-n?3GBK~Z@a%cY9aD7YW0B? zdzZbO^(lI!4Q`rRTp|a4=bK2C3YxAMcJ%9xw%wv;h%uD)+vach}2L zkESt3Ltoj=Mp{FT70Ft_-D0_I9F>rh;tF_yr%!hy(+QLDN_I)M8F-NYIF=I0@Oo^3YLeF z*Fx(pyT1!U#)fB+3~s@Jrw0(5&c~#>d@{buHPSMfehNS*GHLU;+&7Q=J7Vi`yOSHb zA}nn-V%ueS@q2Qn)DE6n_UHGO$J>6|TXx&L(#we2^?r52RT+^J@L+N}N(VkF*%z(o zgRuz!!8Wv(+(LV4Dwrda98k05-ap`IOTD%##VB_6dQhmn9E=xDIQ=U=u@BWcelKUm zjMsAi@#nGR8{pf2kqRcl9k*xxbl9zJ6n*R-K{wqtt976b>Hquv<(1mW!>UJ2Txx*3 z(_V(ve03L;XX=DE;CFyY)hjajlN^A+WQh~lHSYPW4x**}aPZ) z%hnh%Bk?ad&mo?z1ZG7bZv`iC42T@ndyB^y?qv7HN;PrOS*7Rx8n{>MJFWiwV{r5G-jA#N=Y1EKuzh zeWUFU_OR>yZS_So9Scx7VHnRKlMn?icMD25Psd8@)}y4P3GCo`#OJI?64RTuOxzgJ z8^8mDu1olOfXR);rXvYo{t@hwtvCu4!;c>-`@n3YO!d@JEwjh@Jt&0LjfNydO5{Y% zMTH4sqBUifW{y?YHq%$Mr zp^ciAh6TOCBlB>g7ghN|&Vc8&m*=Rczt{q_OzUN2U@;I)9i+aZE7mF&p-Q1R$jW{t zM#Zav>nH_wC!*6WXRYN4h7$AA9f7)|IdM?WlMuZ~qegg`L||}?j*v|$uI}x2KT}JG zX_}YGDvLJVD!)MjyCWxHbJ@pluaxFNUh5d*$=xo}xsYOtqwc+MZTk#h{B~oeOl9Zg z=CkW2c1d8SyIQbpv|SNq6A{)~Z@+gSNale1)NOh@-c!a%_TkR})NY)IcQl!xVIEB^7T7 zi{)on0M!Gxlv5#}3-OBd>gHf_Q$(rk3DXC%C5!XO=3gnZ{yHbUDvq2S0;1{AXFfnI z5_F}17I7ateWi7iysunj!LxV3G(`z+aJ(h3^~igR-x1>^A;&7eQ|qVs2NEf zn$6|-YRL2uI4jLO@!XX5b7JjYjR+DP&xDii9(848`jr7S{91wJu<_VSliD$F(_*!5 zIHy!!$4*P~3?!1+<@}kP@T*9kE_0Rp zsTR)7)@@HN_TEBAo#|#NGC&IQ95O+Xv5q+`sJ`!ild(M z<_~1EuJ_WGlOu^=^PIKfxVcU+n&J{p*!;zdq*z~Y9T@ctvph=@e|PzrH{gZUOF9ju zVplCDQJIt4{jA0QDMyWwerXuf&;}2{}5yg)_b*p0z;o8Q1%j|a4thQ!B#0W(_ zj4$<&=caFC2k|oFko-_0t|WdtrXZ&UW?6tL(VTEAa-43A-#LN%n0paNj(;>_q-W&= zO5fU1f_N}liAsK@BYV=eoJg?!v1GS8;t=D)TRXXLDZSIYeO14US!mD z{WEv{MftLiR4cbf+i7{&hRIpm;Jy~GSY|NM(2na*X08%VLZWcvj>@s7G_JVwDo_W7Ax-ke4cWQ87=})PJulwx{qsZEuK4a8$o^CvAU`Douj&^i7bYQQ$5Wv zgs4ijJ<$~mSv164Ru0a7M6>S#v)8DdQYA%GOPX#8#lqg2Z|8M5bfi8m_za!r;exu5 z;*9?y$b+}m^sAJAZDi^lWkQ%8ZB@3GU)m*d!S8FqZ;IM`V0AavT3~6K8O&f zD%5m~nk*SF@NPO)8y`2a?15klf(K%Wk>0}Tx6G~nC-kU`OJ)KO6ElgLzTM2VJkb*k zXp!^wfd;R0(q;~+0j}xAMakoC$-|E4I*)7ercNrL$>SItkr%E|5;Cob$~jXtq{5h0X5J=G^= zH`0%JYSZ}HV{kshiL5VP6$-CUz?5rz5D|I&2h+Q>*;>?VT6`-H@z#Al=Bo}pJyXc* zp?;5aF23GiMMp->q?~}>>oGkoc!vkN`W&@hhpf&Ecj98S7$Dvlwes0vdZN=mBQ(Qu zpHxrB9_LGKHAtCiLJ~bJg=?SOgj6+QoVf;O!OA-GQ&%;770bU z5~L1Wm=I$*wXde1o4$PEZ>o3O`Bjn+SKqt6s{ba%wJIGM&iqyB(br~*TQjUI*c8NQ z{}r(>98e@@Bkr*k9--=Tz49Yp(3^rSc&Bl z7J}Ew?mtWC--(PDhSz+NLQ@|nDeHcbwYbh+8qb0iV2t_R!DZ1azC?HDx9%-_m874O zK+1Goj(3lI6l03ZDdNm+oV+f6%UKxTo=xwVBR)FH20?^AHN3D@&E@Z(omH`K#KyyM zr&ryQWdXyb7n&3qovZGOu_uvRP{pwc@5zk2?=0gi5eoXKqg7#0o9Zz=U9I(^(W4(d z&=HG3WA2`jh%##GE?G3AhnHoI^ceTZq(PO#zT*zSgfqAPwI|s)6scbQp8fqiCfh| zD+8RelB|!9Nf7BY^zIgwN8Q z$j^;OaMX^1C115E%wZGo)MjG z9+*oSs&D=Tq7c$YmFMO46^>1G!&myBseux~y;gRDG(jF1C7qs*-KnW7{ixx}YFef{ zuF&LjS_DrE6-tR6^&C5(!Rk zJ|z9>wd%NlVuBEnB>3Xc4cQxvW2W%_E z_0g+6342L$2|TJ9-ByvxQbnTa*CNfZC>ER>O?(YYK0Pnf>n>NX-$5IOzrzT)or_iE zEA(vkGOl+l4hxsL=)ZksU1TL$!KtayL;2p&&hVF4)t@=jjo>*d^^Iid>yLBi8&2QK z36*v`VcjEr8mUmaMC>g5vNe;klL5wTdTXnR=8Neu;n~uZIFi0^H)F~hdRiS&J1@_L z;*97kbE&IDf%{Cmpho674Ufu;ZkF05O^^L+jpPe~K6izd%XkiD)>~aB((P(il@cp2 zcL*1QS9yeuYBY;>)6b-I7AWWI9zy>757U>ux1MQVSwytORp^%A^q*NpssQ8?3FO{wW>>D-M(OA4Ood>f9DQXz2xJD*7FIbb}?5?gW4Jd;0q z$E3iab9Y2#T$z1C3i108a{pcqMxU^rJOVqzL~-C^rSGT**EuaJED{v*3Ko$n5_%~S z8WwI{ud!XMtBxtc+HIW>f(g5yBd{2Qj;VvW+WQkzV4IJQY=q9YtNBEw>Rg>MU}JKN zT9FvxR!nd1io&)?7LYp9w8%q(%M@u1!E$?ZK?a3^!^%VPoiRk>hbZ$nY@0eE`O6{t zam#J|c0d^!U&0=_`hN8J==v7{92UMTTv>_7CwHiU2>y_WOaWws09G?(S>0Sx2z-hU z3<9{qI|v3I&?t6j4uw7@92OGtHUlXF((@poIW7L<2du~zHBn*RtpHJo5|;FZHGyac zSldj#Dogwm%dZuPo*%J`^_Ng9#ZWstwTrVbnjQMvv2}+Y`~h5ue}_T*1gX|f$%1_~ zkIvo$FS&^IT^6F|N}|n$LiWFlpz#P%o~SC8T=MZ~?=XBLDO%aPBl)r%0VFuA?@*tY z>DZ882?W!`;EA8<9>TE#viAJ4T`ME~EQKd>amVLl-+4*Iu3 zkYWbMEhqETTD+SyK}c_ahS)|GnSriUPhzff-|~*!^2RP`IMiqiX+!B zz^uZPek4dMQW{W5+Ci(KHl1f?a&DaD?OP8T3myxaW8nGr%W&p*SW-ZKOHqu4sEMTE zgc=_v*)J8X983B4^?5Fj_cv@Mp~eps#Bf-&R6ekug96Zyl!ATGWkG()EtNvGm9Fk+ zEpnA2KQS9J7~kes34AWa$s86cx_eoyNNDiB<$=j5)eL|PeAf04Of&W`J7=(||y%Ix0GV>3sF?TWG*SI*eEF&8$kdNdreG(7__wWcIAV6Xa=En}4Pz(=io8Ff(G^~D{GI(Om${B#l zY#Yy-+$L|-%%;S32JbRRTBz%zeqG$HIjy$J=<^M(oLA|kXXmAvpHv^`)&kl;_@7v7 z5J<_*1gJPw)4ka}WM3*|+^D&m&rGTQW{;Hz=gW>BLHCy67Rgr@nVry>W5m33->PYD zYTT1&-XgujbcX}m-LO`;V!r-O(j`FiUVy=HE^MdjpmG3_@1T!MUO8d%$D?c?uF8_h zugT=)>iirAyMh9{43jOWg@)th8+()QYG$p^Y>?sUK$EjjxlhByn;s9iV0#8&p zDnEA_SrRF_6x0W;&sl{#I)2W{J9Hi|T)MB;7C7k9)E?}sq?=|E7~K6trK?2|20@%V z@IKFWCh7Z&Kl}lF&CC1tcq;~8c{C*qUvKR1o^842KIf>M7f^^0T*}aROa|GSq1Po% zg}{PZweUc$@yVQ(>z8E6>m&fp*Z0*((|ogd(-(;|Uh;5A{lRx1cC*iGkL430|FqI1pH!G1Qeg2pqct=%_CI|bf526>i*?3ho%5}fWMa?ssnRO`V z-xtlF8s7%W*p$pEP662kvfqyHw4{)qG>X%#WASE9#_ehx=Vz^>XJ;$l6JSMe#4cOo ze_0br*-4i>V1<%^vjrf|4}Y&gL1jbs=L&z_LjP*KlHTnD31HXe^?0j%ST{+u=#AQ? zW#VTmnDi+T7YVA@fxKP6&6GQvMz1PeTj7$n_Bi}_w%9hX8}ahvc-by1#n9J7AxQNW zqYS+N8K@v>Y0~+AeH+0qtGrh^ofWtEz<#h>-3_8L7F_iDXup^#Tk(Re6B@uFi2!iXC~z z5IOTN0xBxaljp2;DwoONCdm~&FtxpicHQIOZ0C8Re4Aa{^>(3tylJaTmD3vO3p!V%*j zy@bfb^jw!F@A3|I_I*9+4H$_OGQ(Sf+Yf6VqiIoVk zsCm(P;6|IPn#zfoo7=4Ia^FTxhV*{zao>JZL%uVy6mVf#(N#SJ;Gx@AL z@eSa#2m`PGj#OqLWtsa>eoN>QPH#bIqj5}`SlC6co_FUpCo+E4)M!c2g6STin&?|H z<^cp|Xu!?(2q%&w6cUtN`RxW>VRt7TeACpkLjY=Zh!q?@#GF)61JllKCjfer zA?@8#`1T+BQBT%dl|$qn^Q{%vx6Hg`CRy@`Jpgv>9Z#Q7dK-eHVhaf7E(V~xvlySH z?`W*V6x}zo`vZuQ4xtjyegrLz-gNY_hN1aJHu=W+F`0bU>tBd)@H9TKtj8iycnDTJ z)RN0weUEd|QIQMGXP580-|kKpG?kSbbfTnDXKvhVrMWOQoqyIAiyVO_#{I!~_FY~h zCq(K`M}xUR9xvRO9M5QT1>-vdhQbUB-FH+FW<3Bd5`B*n_-@-8=(9`4e2bt_J2H?e zFuw@?&%>2KQfDyrUBosjqa)Tuoou;^!G8?B8WF*)lVWO`Aa)!PI`qepwFUdn>{Yew zzRiOu?c%67=|NX__hLtfKNCd?r)?hn0Qrhf2JNzuOmSpLZ}~SE1hHVB=BLZWmWQx; zU{R)}xp&dysL2{6YPa4Q%RY<{!LSQ{!*rQ~VXT?qBQuIZWh9maU1t(XLXKOynnrR)6I&m~pkS&ik>y8Wtj&z~ef%W+inko0kd zbirv_d+Rwlo6BL*a^1u^ABu#;k zBa7j0|K1)w7DFn*11ZHJBgaq91|plE5@-JIu`a66Xo4fKBVSuUFCSEE?@#*|=a()y zubQu#hEu?lsr9tB%SAvJvCG0r^J#2Jt>ELDpz|9Y*9^YZ?-XJ7ezL@l^>3tm@? zTLRQI%iay)AhQ2xT~JhgY7Z7LoQ>0hC4*PN*z!GCa#yq#gh$vEOTT;hFEaCl$?G~5CGE%#D2M!iw)^DeW- zZ)z^4H1Ezq!t&M~!irgwWV_Hj9CA3{E`)u3UBmn-2X+4?UV2Kh2n1NcTAP||w-N*I zmLDJ35Q935-Yz)bS$VIxB`}qIo1^m+Ql!LPM^?XS>${z=GOE2l4ir2uxm)x+8(fT* z`><2ocq&Pg+1Um>_ja8-o`G2yXE}ijB`>;!jf!UxP4>|~W2ZV_4Q6uAO>s`*`fzUB z@qs*@17iBqF5qr<*n4xjIFu95{iSa$g5z))LDacVh*bzG8MVBlklqLaApv~0RX>n@ zdPnubX>Hn(jsP*qAiIk6x9-lVUXPjl!^EB#eJqYUT_$`q27D(2Icl~o};yxtG;=`wjEtIRsDt36<_3Ihxg~V`RaMT zt@79sz!=PEd@`sNW3Xs%+|O6V3SOJZblG^H5go;r5;cQp;Cr2uAk}N4#@#6z()U(V zu2!%OA$X{)UyMe~2*2gHnxjnJ-<&lGk(UHs8vB^BG>ldK)wWq82UXe+tP!e=&Ol-| zcP06$9N#(mmV?`0)8$^o$!AkV8UP>}7dWV@rr;JOu_?O5l-dMh8 zWMsaoGJPS(rFSMmmdzG7K2@wfXik(60m7_-a9)4p(sH->h_1%BU+U*Ep1ibPJ!?X= z{HbYRbr~$$c#vXTx?8%swTJ_X2||nI$Wjs?=b7WV5g;Rs&jE-t-w%F--yb1;(8j@r zB7Ra7Rr7bbH$kgixodK73rlhI_Vd9!ME`fgu<& zqx@SGTeVV*bsZib&yR&shdDTuEV?U~_IoJ8&-c3KScDfd}nRV=!f~vaxaq zw-~;xa0<<&JniWU+N`XFvQ`WvamdCoI?HM7jss@y$A4Zv7&sRljNnKUka8_LM(e6E zh6j3!K6DY;IED*8)&k=)+b@v!C@{b`3kUlY^VNrK&z-Db0y9mZT5_D$atH-PQb~*& zFRMzNa(~(fd8|hQ;tS$}w+gv0_R?-Aa_^b4x=GZOgZZ0^P<(%RcyM{ifs^|q$wi#| z9ziS)n!FS&%d>5erAs_z;ka=_i`qBtj(!%keOI7*HWxz|J06brrMY^_V;ir5CdDxd zAd2&TzdvVDa^X;4)vD!WjuJOEcwNeAt4+%XmnD);h%f*kjT$&M1zmw=?6~B5s!E7N zQCkFe8b0FL&ZjsG@r-*HGGD?J5KinE8zY%nj;^-%sw9B&Q8~t91d&&UdvKTfiTL=E z)U))-j>+RrdbIPC;NwLXSMzAe+6YVJ{-bLG#r1hb#rQ6m{7eX(?&d+iuDup& z&Dxu4Zs24t^@^=vAJ=!gaoo^MU5z8nd6xoe8Kq?CD2I@Fy@{E{IJaiTL@tn6RK)&* z00}JsMzJ&p@vR>Z+yn|N(sv(zh_hdf6*fUyyoaY6rEUzhm+_8@YK3RC;WGdQMQ|KE zR#dKl#hb<)LPn~TfVhy_lqu!UOblbh2StG4e%8lUez+UXnkvq$-n9=hRbvx$&j-X8 z7kQg-D(nQ?Z9kTrbq)>i%|nR#`4uk~rVqkwfY9cIuRq;4CX(phxNdF1wO@C(S${Uf zS7Pn2=HA4Q{S5d}d`$0UOT^xNr|k=6flaPg`#JAThbZw3GWcdtkHqC>wx{;|9kFr5 z`p4OPc4NKZ%1GIBcwQ8&@9b}rdb=a}ZX(Q`I^yP?=>jFMNv>jLF6iVxy?UNrBqV*0 zLL?%RJI2wV6#?{&HNf}yB+yTa=y4+KqPkC$62Tf=39XL*i^)mWCAbZv~QVXm<`|jHjKE*#O%FFB7cgaGFqc3tf zgPzgLYNRZlf#c=->f0a82oQN;s4k6{BMbg8dGa8U!*of4EmPrmxKk`HIf&KyKwluu za2y|+a@7Ww2c|h}h1~2JEg}K=F!1$AzB*VEXAEktP-(lFZ zWSSuEN;bEko`2tx_~^afZEYy+RkZy@GzTv%^qjqqel!KI&SX-|WoQhjvo3g1wex$; zBryh;44kK#Gz5gC?9zI^L9>yiJ(K$&xi{+WkgfrxQApV=GSQBm;#jSO<5@?~|(YTa0%UUlny0rchhCGc3-L9%?T1 z%}n*pHCq8P%>>=$r#UnW0p+iUZA5OEt?dWL-s_&CkOh}AiQZD`#uf9U7WGJQE+f*X zEm!|yig_2LnPfFzB(i}UqH1NcYm-+aJ!SNM`TX0RB4KiOp_Y1bl0`yPY4tm*5V=nS z{5qk#syY+`qE?L2Ocv3TX_GOcRa=VGYx?~^K-~o#V1rQViK@>fZxB1eZ(+UNHv)5e zb`KUD1fyNnD^;%AZS>Xm%*y?fRsh|H%?tn2F^jv;CU*1%xCQg4Rmur;^M%Dp)#QsD zG8x4hk~+@6i}N`()U8C^_H2pc2N2?`L#B6Evh!mEZ$WW?UY%`*t>i6DV9bB z$@Kz(`LBnogRFm!DQ2~^%kD%iC1H$b)#n7hOHujIlA^9ElRO!{ygC2-)?^n+lcmXt zn*K-r&-{cj(;iyBzc_I}c`tJ^ovo8F(s(oP&v}({-}F9CRacWvt#im28%2%pM!eFk zFCGx5G_0>M6OjILc9$d0cX6(f!Q45mI($V1SOA5M8?(*!3`Hbo?(Jvf^6ZIQik^)* ze{RzS#osbw8Z=xkdyVB_L*!nxP-I87fkJAERAu)sQoW1(Z=v%+m$HN_EXFr|v_o)j7Z z_*|klm9{Ok*5)@w=js}Xho0`Vy0}b|Y{YocsZlU6v>2a03Mo}{5X{pxffrR*on|y{ zY}8O&>{OFl6K#C64@xeyVh~)wTob*C<1|dht1PchA&aI0F^onD74t!(zEcK)mWvCc zA8|{P#LWZiX{A~TBweV+_D)HSy>kF(n)TfmTUwkhnXa)@&HfS_nitZ=sU%|dtf59y zXZ8eji|$r8=l9;)BjkzDeCY%h0wxJonGJVpDsVx{z2-STvy0>~YLS>5dFClq+}jVn z;7MUGg$`C`>1_X0f#rJ#V$PS$uEchV2W-Xu=LBbn|vf3Wf-^aa{&~qrS4a* zC_-HF<5vHHld=3-VU93al9tQDeqS~i>a{B~U%HZmm{%>vr>v>t4zgE0P?;L(hIh*{ zv+1ZQAKPf2!b)C z5#}9oQuQEe|G(du-y;~5P{@qK5X5B)@j;kF5ST{-Px3~n|GffQ{?E_PegoNTg>QgJ z`ga&bTp&0v0ug*hIupXVpB1RyKm!Ciz%$nAUEh1aIP5Eq!&5N&rP)-{s7!rnx~+%u~JA_B~29vya-|& z{H|VR@C}R(nzu+ikA;}bb~>|@ay3A72Uya!@1?Q^tAiEJIZFtT|65m%=k56D{#$cU zM7Bgc6$-v!27SQT)AJ-)*YS;(t*-~zV!;2Qe|+i#nJOsJiP(-H_)%w3(ljL_Ee-e^ zjQ43In#0Y8WqWAPHqSw{PEwxsT8L8VK!p@#UQz4jIFBEm!KPqgFos~ zSNtYaxZ+;S*mRwv_)CDR6mrwZ?B`A-bT;3al6VvJ9%fRjIYeI!0I382d=AFrV6N)0 zxZ%)#-m)T8v%)>}xmJ6j`R-6NO}5H@Lo%(}sP|P(=WEUDslNV`3u$aL#rd)NQ3&Z>6|gx=kUol$FIz1Gs z=f2P{$G<>*ea(8CSv`GWg7{hMocTD2q4g6*3i_m0H-T3;~lxt+i(7xoG)>IR*I zO|u%ae$i%RR+Q$)i;{+`#w`IS_>b@p{frF*U?YI{VF!-f`FN6&3Q2I<)BTopNzpSv^ zUYE1oDQ=sIPAO!n7a|8YV2RJJ^Z!@Xbq7-Uy>YIQk&Gt7Eqm)mWnWulWqz|m5t6dk z>t>b6%1CAJ5sHj!6GFMRvJ#4{aBY|0c}w4~pFi*Gyyu+foadb9`8=QJd50Y@kNd@n z1N!_Z^Vl!)a;}S?YJt01dC?rolH9A9oKuczxBZg1ef7X}hR_yVaKP+20ZE2vQbH@Q zL{^q8Q2O$xIE2F%rezxT$UttpJz&0*$EapWSO$ zUoA?GZ#`vu#xJDx=+wLY`P;IqbAn?lape_+jq!12M!@0T7^U_5MN$rgcg2$#Cd6|K zKRJqIgs4{pRF#zj8E+(>=W3d9E%JUPxjtm&OF%Kf{P`sv9dXa|*3E6d#Ah>NURJZ# z?M@2=C%oUak&?TwB;kmn+1c6H-3zT}Cu!(nw-(*&=7!z-bilXaBcyspJzHNb>Z??@ z&-r|N%sm1lT~9sw>BrK0kF9!G2p|4c%{ngsg{*F7LZf2Q$4Us&iZ zQU{{fnEk@fE}06)#G9EW;fJ@(PAn$<}v1QpttdkS+6djuyH!XG+Tpy*y;moZP-Abz^5z=$nM6o z8eaRRW^yNFPQg*$G8M1s%*c2p-$?pQzkiIyMjdt*$~W$#5}y^JyoE=xGX3f;A6Hc3 zB^g<%I@tbM9BdaD_0AptLHqZnxJdbx`1yxG5U4`3-`gi_cw3>(j%e`rAWeItMJ)ys zX*_x4*_%>MW=^kWsNXk+oNCItP$U*_H7-9`{n52S9^5lf@XzF6plX}7bxvE;P36cB zzH^OE;u@{dshsTcMKp2r=^`bTjm%ei3R9}e5=`ebN?Q~ND3Xuw_^^{tCA|VN4cfL) zl8eRZx7pcM$vYd#sB7fX%8=z_ZuuL14%mfvwG`07wU&;C&>I?9*_IhCppo|In`gaU z!uT*k)xvI=?yqL7MABP^;EOwAIfKvmXa>rJNfb}B`PQ3Vw}qM7P)_G+a(=1FlXte(wUbrM3W-*Wt{X34hx#Mprc_X?e; zTm17QH-eY~|Iq(b{5Zh-7pH}xO7B+5v57IlqC3{TX3li6(8)J{gK5NXXQB26BQeuZ zk%y&{uX%5jo{Y)qw1$f68^6+@2aWdZ|&<^P+UB(rD{jdsVz5`J+>nbB;z-eEDu)@P(ca?1ST`I^UjOdQ^=@ z2?*qbeaXqSK$)C4HndN)MBh_&yiBgQkK&I>gV8x}u`y!gtJXtu^NE&~92)Ey5$Kga z`S%;@sSQ3e;gLEiqJpG@Urp>eM8kjnbSQb}$@!YlUp8h@jHwpQf0^ZI>bEt$w_uc5 zl@T=R)zl9-{Zq%Td?j*1+R_z?olE@0!LDR4BviY3b6@;dJqZCXT+?n?*KbMwQSJ!K z)a$xEuMGg<^a{(zyMHsR+m6L_vvJpNAi7QM>Z37TWG`gM9OQqtJ@swYJXo%mpZLg{78EZxkwPv!^qz1FcyscJ-rb#oT8vk9k1pmjy(B#Sx!3 zrD`ACSi~G*&Dl>1sV3fDpw{S!E=RKxGoD?!))uKf1fbR$x%B%zWiGSVWrNfMTs79N zZ>M*#xe9xK?%37ivoFf>%$1pq`=LPo(5xHlH|kZ+u_H*|pIUWdu+eok+LA*n&@7gp zUA|%9ZCge6d9T@+uq;RDc~}^Ip;Huwv+K#g^O-d)>ZP8UY-5fg(VSSr%KbCPJ>{n` zl^eC*ud>V^hpdEUIuQZo!L-xkny5UT7C28a70p!Dm!;m%Zd7$OnsoZ8ij&q$?OpX0 z!0HSrpQ4=TKCVV{2|p#~Bk@A}v@IX0mwY|+>aA1h*mlN?@%Gb@!L|qXN!mwmom*dj ztlG@Xq-9V?%WV0$8rY$b_*Z4KdTNHB%j^ek8D98tgrN4eBieMxK`_`QzuM(}e6 zA#Hzgt8hvHa}L#~IS&FXM2r%@vy+PPFv?zEZRlAc@%u2{kyx-Ia2IuB-^Q-J+``Va zvVbOzi;*yt5=Q-iG30($x~;v$&y|?9;o4L0=0G7PpP3L!N>~*W8E|jopT~d5dQayM z)$`C>Pz~^~Xl!5iL|hUX5LMD(>7m`@9a}t6&>k>P=fC-)yC;<@6vTts<^6e$*zY4} zgSjm>%GImBdi@yG*b*!H;jT%72}Kw=?0ICFFLqzKghwD)=J zQLT6mhiB@%*vIoyr^g(Cd3U+&{6)PhaN`Q{j<8ERG7>!Rk2`yyfL{RRY^zrk-6IoR z$_o4tfA6|-pKbt(!-LCV!n9dE0f1Z5FN1~BY^d(9zXcwWoG*43cuf|aMT|mj>QQ4j zC@fOAofB+1{#ukC5U}TWiy=0(DT;Lz0D^GV4fPYAK12;<@~xwqW27iJ%cC=|?D%^CUUz zjf*8XRm3Uug3u4itC9+u{i?3VHfhKncr!IzbUUol*g+a)SrDu+M-VnsHL-Nl$&ZYx zyk2(ME2gr1R`zNOfZu70s3LDQE0&y*(UB#+Ii7t1eI*;lSZ|th$V;I_GU{@Ubx=do z#*gzyU@Llav%q2^1$+P;3rD6$LAYy;SD(-Dgn%z>*(dMKCXYb7txyFG z^Cb_R4t~DdcF}h4hkI2a!*r?s$vGaoy0Y}{chCLu)1!{9{JouFs!2xdi)}Qm-m*u& zbBqLUuMvCYC~6_g=duk}>b}i*#2dU@eoO<>YC_$7JKFf|t#DzXrS~9Lc@&OnY`8-k z?speQD#ySV8FlR}tGC64LNS)%4qp2^u+V%n=lvj^YhL&A)?|{qf^hqwPbDEp~ zv)guA=Emx}erC#^RCU|^MI%3C_dQ`-e%n`lP;*FmA;j~GVOz(LpJ03)-3&2?*eu=) z_rgX0{@EUBx0820yL}6o&jL4j)TKh$M$)rMiQ8to=XoIiY@FTYX)NzB=L%{A@#G6xXj+mzzcX%3@9)^Sf5jZxmSV1dks@RfcWa^B4Gz$+&wuvg z|7SIjN4R>1SIA1GfqXuzYs$g=eyI8`IIB15V?oxPdTR^{SVHn8$;bIwxm5KB#rTxzfk(0 zr*M%+2dwnl8+ZrE&0Veq0r)EW*Oe{aO6#Le=1!uvbi%@zEC&|Psi-wM0{EcGS3i6@ zTP4Y{EAU%8J}zSfdp10EKrGLpPM;$%a? z;x=SC_EjgT(Xo8=`)Zt!=}$kxoH-VRb#OJSua;b1+^x#RehcM)*(2sU-zvLTav1@K z%FVlAQbafnBVuLV#C_Ct5k$i9t{`MNw%BIKGJDFgtZn1R>AKCJu5oCeFlrNT`aQhF zDnfd|5oEvD(zuU#LfKB~Qj&M}^Uc=yKp31M`8IU7j^I|L&@wE9o&R=B8n2!0_1A#0 zWVhHDGFnjUC3(1m8dz58qF}JKF;y%djSIQC`tcIX{)pb$9;{o0t6~p>d3wTRid<&u zJO$+s!2JlQDwnrIp+8BjUULUK(RQ#y7&58Ajk@$|EDG;^6Rz$~!FTz5zqr`MwpWbZ z81t@KPAi2e6Tqljx({Os=Ebkec#3?c`F&a14J)SOj7Pl3cg!~SHtNQGe@Koy^Bca# zyV}Cx@d3ie@!jy9xw<|3rAl04EC{-P_MK!N|FwgX0b!rLACMc0XQ~^Dy0CH^TED#* zlf0cVo-Ae`hXu*I)luo&eYA|n6BDz3fwAD?^@l-9`;rjI6LAM}-`VrT%y+?Fs;1Pk z;`pFo`ACncYrp=ONY&om-<*6*WJz(4Z7<|m5wLL`u6MZq-DzonF z4w9F*34_3+979HxEJsE7G&7U$Z$|9T)Ucqgxs5ohYyiSQ6)26}wxzV`3X?K$rHCDH=D^m=O)MwfFe#YV+9Oo zK)Nzk@ZC^a?Q2DeX=wS%LSJw7a_Y8CLED+=l5sXsGC3k z(Hxb5rwk7|!s=1)ebKtJZBv`s{fiy9JE(AA)BDM9{`8mpuuW-@=#(@8SW%4?_`4A=7)}E7T*WS zw1yg*&eql}+8$T!&P{ET`%F3Z8jrFBc-!uEqfL{J(rg;ml(xexurZ#>nY1S(AlPqO zq=0IV{~`CJQLs%U>b9I*RUcP>y5046noT`6zl?mpv?I00r(C0m&o6!4EO3?$Qkzqm z+WHLL$uIOF{8Sk_T{hwF_rrYz;CxFRd4BzZR+EpplYV*GL{?9-EZt+*|G2iO=(j!gbH%$TkSCD;MW$X}^(@OvPm}er>}G)MuQ%=S^Dt@3izHX3 z(BZn1kr^SHKF!Yh2GVIx9z?)@fTZlRvLDsf&VsTDiw_-`1^^7U01cMSxa*b%e_z_) zTdL8y=@=sbtYXzL{}vwI_*czLS`W_Q*kD#I!BRmGxS*88G(BSP+sId)d?uL*7TXV! z2``hQGr6~-qqBl+-Ar&fYZ9GX37} z(l|x?YXUi4d_}yLH_t^pvW~d&jbcA4HSMw`0naV#Tm%v3d|7U7CL9A?GQhYJkzo+m z7JU%;=SK66{BhJ(A>}S|$igfD66_qiHSD;HPlsAry*s^+i(VO=ySt{$qhZ>^6y~%y z2-&oBaLJhSh~WlO%&3dkF7Xv|MrA8d*UH8aa#Bw@cg&^C8aesS853R<;6qIcKS!pf zDE&hD)gKeGYc^v}Kv`R1$S5o1Z2+tz1&cZSDb zcR;x>&6d6A3d^ftd^y`bH11TbeMxmS^NlG#fSzFM!1?G3pVo`R1#Wdeq?eXiBUISD z`@CaVeepWnx1ONVo5PB{loStaCK9el$ac{2K@Z+vge-q}Qge%*E7JTI3pT%sVwSR& z{QIdXMjXq%_6X zPV6i5GqT^XIzK5dr(1*fGA-=Jo#4{I_8`rZg0y_T9bX&AY41h69cTPtF3_(`IQFPL zNRE|~k@7pS_&ly@*FI=%AiqPXu5$rkwu`{uuI)uQYY4wh87G3eUzCsSe!sjEw44a) z(ckXl_gga%Vbr{tIxgRx=1EYiJ}Oo==ey*cytCOVyBK~^@SP!c$~h|PyjGNTbI<|A ze?|#m^*LOqa-y1?iiyZhEm&jj%=-X^WI*R3-go^}+s({%X>(Q{#enziF3o)1G*x|T zw%6%Fjt~KgOi3Eze@*n^3o}{{X%9ox2S(CFd8&=(E zN`C&Y*%(Z)w+fAm?_qk8=DkJps7#3|M;vp-ln&S*y771_W9p80;f>(1EHv-9hZo+uusLz$&Ra#u zevgwb?({}a)kHbR$&#-m%^W#BO;69=Uj*AC^|o}lCK?Uq{xZ$M^QQb_jxizB>CQg+ zU#P?D%GR%M2x24L8bQQLG2krC9TBvI_|!8-8${Kane?ScGCDMoTwg7A zut!j!%E!@190!f=>3b(|;| z$|l}cehi_z{3u_Qaq4DYR=(B|jA8uJayS3#g=JZIsb4EqY z@m046Lt0Wf<0lSjq_lTq#}*le9!cjX+sN%`=|fs|^)x(n4h7`;SHb5`d$j?n63oSt z%HTsc2O~lpQ8f!9Zku}Vu@~g^-o03<%;KzphABfd+c{`>Z%&54a%9hYnwJ;59)z-# zx}(6LGoY~#oHcu?flJu+^(q;>LJAw{eDza(L%{MW7)w8bU{^*f2mAJwi9h)6L1I1K z9$TB;J|`OJCE0(0Z(3qv=<0KwiGD}fuvd=Q{=xi}!cyU9l#?}gdPU|Ivi89Q#g0Q| z8ZE7$_|ga{#xt_%;g}=V>9{C%9G|U)T;F;z#T1FBWta9xLliULFFTD%LF^`=7!pBw zDEUh+8L+PbE(AN{-eISOOlH`q9zRS%wz4+s#|YK1k^#<0`n?mgB&1XnGs@W!)kvzj zfx^{fgiiGD4FYlqP2H!VH8?M9!+)sE$QVI-os(|ZzlG@P+O?EXI$L!8U5_ftpBKH? z9U%6;QvadVE6kz+0`OjmWd{gx-JYgq!yEhi(-ydisB^ztGCtQ0c#2L@t`}l(JN|4r zwIYmmlnww7x^S2Hr~anit~F(1$O+dLv5cq}w`=jB>^G}S*>PM8(mu1ODH}O#u zmdO~g(BJwOc7+)G{U=jFh2u$X&@U)A!i1FGNfrA<}gKvKd?IGd>g1GSFi?sXqz}Dtdr3X>dkhDy>W?C>H83zpAunnkW%wHj z`K{lP2nAElx*4kq`z1AtK^ zJnSdQ$vHd)BGOPw5i)g?&2x?4-UjF$e*aN%jA*t6jksRoA!-B=*oouyM7-bS`3_4D zi6em!?3UnLHJS#rnlyR`hvV;lkuq>ta>Tf}#RQbL;TApoEG3%MgF5b*Ote= zmH}>s_E|N_!)gWLv@Tr8l9TuPkDNONhP71jMAzGt^{+$LG zT?#kpO=0K{y_H*qD)aEW{K3`*qrK=QcfbZYm_F_}qU1Ixz>l92Nj-76cYs)%%dZ6nIDPp`oG@JV%liooZ?NE$SF-@rh^ox-}V6y1{(VP zj6vE>l=R)c`b}^%5P#|2>%r1S_k`&60T(p22Va(Qg?8$|8{MwFai$#|qSk zCXgqGT%S*eIYF|)*TZ!~%nmPP3QVot1Nn1`dly>PBM_X28X~n%BBIQ_2^3zzfC+wgBWtLbI80{QxTev!20m==l=xt=~2rD+|-%wE^2195!&8cV8sk^TRtM zM}&CnVP*iE=ecV^UEEa%SpV;B2vVO_dq^0(S#Ov>)@A%})rd&>t|6~NB!Rp}CuOW= zCH{MY?tzsFY;nA}%H*Lf&TyX2dtE1yyv;~nB`oW(7?3uIs6lg(^P1s9WGDIGoBs4c zV%<4`$o(a*K6vP5+gFh*I7!ey$2#`Wj^}O;_2E_0=K}M*K24%Ley9l&rv*zJbff4- zvbIWe{q*6Uu7lD2)^rC7K=m#?dH8j{oOD}%GVCvFo6{m=4}ukc)*mFt!Dk1hK~R^` zVaY{`0t*scES$cdx;_3oQ!s%CHefVvIW70_BB&w+r{G2K735GV&3u?fbm-{CntZ8KIjccYUMBwkLlBVKod5eet1D?Br At^fc4 diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index 503a00969d5..9c79d8c457e 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -20,9 +20,10 @@ The list of supported autodeploy templates is available [here][autodeploy-templa ## Configuration 1. Enable a deployment [project service][project-services] to store your -credentials. For example, if you want to deploy to a Kubernetes cluster -you have to enable [Kubernetes service][kubernetes-service]. -1. Configure GitLab Runner to use [docker-in-docker executor][docker-in-docker]. +credentials. For example, if you want to deploy to OpenShift you have to +enable [Kubernetes service][kubernetes-service]. +1. Configure GitLab Runner to use Docker or Kubernetes executor with +[privileged mode enabled][docker-in-docker]. 1. Navigate to the "Project" tab and click "Set up autodeploy" button. ![Autodeploy button](img/autodeploy_button.png) 1. Select a template. From 12a088a15a88d2b2493847d9d33f6999f366b928 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Thu, 22 Dec 2016 00:33:38 +0100 Subject: [PATCH 157/206] fixed minor animation glitch in mini pipeline graph animation --- app/assets/stylesheets/pages/pipelines.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f6164c8907e..697887dcbe5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -827,7 +827,7 @@ margin-right: -8px; .ci-status-icon { - width: 28px; + width: 32px; padding: 0 8px 0 0; transition: width 0.2s cubic-bezier(0.25, 0, 1, 1); @@ -909,4 +909,4 @@ min-height: 450px; } } -} \ No newline at end of file +} From 0a2fb360eeb38afef81b40e756663dd301543fad Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Dec 2016 22:18:41 -0500 Subject: [PATCH 158/206] Exclude non existent repository storages. --- db/migrate/20161220141214_remove_dot_git_from_group_names.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb index bd0e4b2cc07..241afc6b097 100644 --- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -61,7 +61,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration def move_namespace(group_id, path_was, path) repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| Gitlab.config.repositories.storages[row['repository_storage']] - end + end.compact # Move the namespace directory in all storages paths used by member projects repository_storage_paths.each do |repository_storage_path| From 5961d1429208d69261a23811baebab8111902591 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Thu, 22 Dec 2016 09:41:53 +0100 Subject: [PATCH 159/206] Fix state_event parameter to reopen an issue --- lib/api/issues.rb | 3 +-- spec/requests/api/issues_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c9124649cbb..91f65882f47 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -37,8 +37,6 @@ module API optional :labels, type: String, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' - optional :state_event, type: String, values: %w[open close], - desc: 'State of the issue' end end @@ -172,6 +170,7 @@ module API optional :title, type: String, desc: 'The title of an issue' optional :updated_at, type: DateTime, desc: 'Date time when the issue was updated. Available only for admins and project owners.' + optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' use :issue_params at_least_one_of :title, :description, :assignee_id, :milestone_id, :labels, :created_at, :due_date, :confidential, :state_event diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5c80dd98dc7..a786dc9edb3 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -932,6 +932,13 @@ describe API::Issues, api: true do expect(json_response['state']).to eq "closed" end + it 'reopens a project isssue' do + put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'reopened' + end + context 'when an admin or owner makes the request' do it 'accepts the update date to be set' do update_time = 2.weeks.ago From 1b6c663cb29ed42433a87a64926d4267c25d3177 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 8 Dec 2016 18:52:20 +1100 Subject: [PATCH 160/206] Fix lookup of project by unknown ref when caching is enabled --- .../24224-fix-project-ref-cache.yml | 4 + .../filter/abstract_reference_filter.rb | 19 +++- .../filter/abstract_link_filter_spec.rb | 52 --------- .../filter/abstract_reference_filter_spec.rb | 103 ++++++++++++++++++ 4 files changed, 122 insertions(+), 56 deletions(-) create mode 100644 changelogs/unreleased/24224-fix-project-ref-cache.yml delete mode 100644 spec/lib/banzai/filter/abstract_link_filter_spec.rb create mode 100644 spec/lib/banzai/filter/abstract_reference_filter_spec.rb diff --git a/changelogs/unreleased/24224-fix-project-ref-cache.yml b/changelogs/unreleased/24224-fix-project-ref-cache.yml new file mode 100644 index 00000000000..a6824ee44de --- /dev/null +++ b/changelogs/unreleased/24224-fix-project-ref-cache.yml @@ -0,0 +1,4 @@ +--- +title: Fix lookup of project by unknown ref when caching is enabled +merge_request: 7988 +author: diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index fd74eeaebe7..966db96f951 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -254,15 +254,26 @@ module Banzai # Returns projects for the given paths. def find_projects_for_paths(paths) if RequestStore.active? - to_query = paths - project_refs_cache.keys + cache = project_refs_cache + to_query = paths - cache.keys unless to_query.empty? - projects_relation_for_paths(to_query).each do |project| - get_or_set_cache(project_refs_cache, project.path_with_namespace) { project } + projects = projects_relation_for_paths(to_query) + + found = [] + projects.each do |project| + ref = project.path_with_namespace + get_or_set_cache(cache, project.path_with_namespace) { project } + found << ref + end + + not_found = to_query - found + not_found.each do |ref| + get_or_set_cache(cache, ref) { nil } end end - project_refs_cache.slice(*paths).values + cache.slice(*paths).values.compact else projects_relation_for_paths(paths) end diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb deleted file mode 100644 index 70a87fbc01e..00000000000 --- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::AbstractReferenceFilter do - let(:project) { create(:empty_project) } - - describe '#references_per_project' do - it 'returns a Hash containing references grouped per project paths' do - doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) - expect(filter).to receive(:object_sym).twice.and_return(:issue) - - refs = filter.references_per_project - - expect(refs).to be_an_instance_of(Hash) - expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) - end - end - - describe '#projects_per_reference' do - it 'returns a Hash containing projects grouped per project paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new(%w[1]) }) - - expect(filter.projects_per_reference). - to eq({ project.path_with_namespace => project }) - end - end - - describe '#find_projects_for_paths' do - it 'returns a list of Projects for a list of paths' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.find_projects_for_paths([project.path_with_namespace])). - to eq([project]) - end - end - - describe '#current_project_path' do - it 'returns the path of the current project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.current_project_path).to eq(project.path_with_namespace) - end - end -end diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb new file mode 100644 index 00000000000..27684882435 --- /dev/null +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Banzai::Filter::AbstractReferenceFilter do + let(:project) { create(:empty_project) } + + describe '#references_per_project' do + it 'returns a Hash containing references grouped per project paths' do + doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) + expect(filter).to receive(:object_sym).twice.and_return(:issue) + + refs = filter.references_per_project + + expect(refs).to be_an_instance_of(Hash) + expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) + end + end + + describe '#projects_per_reference' do + it 'returns a Hash containing projects grouped per project paths' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter).to receive(:references_per_project). + and_return({ project.path_with_namespace => Set.new(%w[1]) }) + + expect(filter.projects_per_reference). + to eq({ project.path_with_namespace => project }) + end + end + + describe '#find_projects_for_paths' do + let(:doc) { Nokogiri::HTML.fragment('') } + let(:filter) { described_class.new(doc, project: project) } + + context 'with RequestStore disabled' do + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + + context 'with RequestStore enabled' do + before do + RequestStore.begin! + end + + after do + RequestStore.end! + RequestStore.clear! + end + + it 'returns a list of Projects for a list of paths' do + expect(filter.find_projects_for_paths([project.path_with_namespace])). + to eq([project]) + end + + context "when no project with that path exists" do + it "returns no value" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + + it "adds the ref to the project refs cache" do + project_refs_cache = {} + allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache) + + filter.find_projects_for_paths(['nonexistent/project']) + + expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) + end + + context 'when the project refs cache includes nil values' do + before do + # adds { 'nonexistent/project' => nil } to cache + filter.project_from_ref_cached('nonexistent/project') + end + + it "return an empty array for paths that don't exist" do + expect(filter.find_projects_for_paths(['nonexistent/project'])). + to eq([]) + end + end + end + end + end + + describe '#current_project_path' do + it 'returns the path of the current project' do + doc = Nokogiri::HTML.fragment('') + filter = described_class.new(doc, project: project) + + expect(filter.current_project_path).to eq(project.path_with_namespace) + end + end +end From f6de2fc57806a4b41c677f388afaf31984be632e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 20 Dec 2016 11:01:48 +0000 Subject: [PATCH 161/206] Use `ref` variable --- lib/banzai/filter/abstract_reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 966db96f951..6d04f68c8f9 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -263,7 +263,7 @@ module Banzai found = [] projects.each do |project| ref = project.path_with_namespace - get_or_set_cache(cache, project.path_with_namespace) { project } + get_or_set_cache(cache, ref) { project } found << ref end From 4e0db4e9542d68dee3ce19a5cf5fb3c206e7b2ba Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 22 Dec 2016 11:23:29 +0000 Subject: [PATCH 162/206] Fix broken dropdown --- app/views/projects/stage/_in_stage_group.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index b15f7eaeab2..65e5f31e86c 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -1,8 +1,8 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown'} } +%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span{class: "ci-status-icon ci-status-icon-#{group_status}"} = ci_icon_for_status(group_status) - %span.ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{name} - #{group_status}" } + %span.ci-status-text = name %span.dropdown-counter-badge= subject.size .dropdown-menu.grouped-pipeline-dropdown From 87b89c05bcd569a39b31f47180542522d44610b6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 22 Dec 2016 11:23:42 +0000 Subject: [PATCH 163/206] Fix hover in dropdowns, make closer to the mockups --- app/assets/stylesheets/pages/pipelines.scss | 61 +++++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 697887dcbe5..7de6a21c07b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -593,6 +593,27 @@ } } } + + .grouped-pipeline-dropdown { + .dropdown-build { + .build-content { + width: 100%; + + &:hover { + background-color: $stage-hover-bg; + color: $gl-text-color; + } + } + + .ci-action-icon-container { + padding: 0; + font-size: 11px; + position: absolute; + top: 1px; + right: 8px; + } + } + } } .dropdown-counter-badge { @@ -603,9 +624,11 @@ margin-right: 2px; } + .grouped-pipeline-dropdown { padding: 0; width: 191px; + min-width: 191px; left: auto; right: -195px; top: -4px; @@ -615,11 +638,22 @@ display: inline-block; } - .build-content { - width: 138px; + .dropdown-build { + .build-content { + width: 100%; - &:hover { - background-color: $stage-hover-bg; + &:hover { + background-color: $stage-hover-bg; + color: $gl-text-color; + } + } + + .ci-action-icon-container { + padding: 0; + font-size: 11px; + position: absolute; + margin-top: 3px; + right: 7px; } } @@ -629,12 +663,10 @@ margin: 3px 0; li { - padding-top: 2px; - margin: 4px 7px; - padding: 0 3px; - padding-left: 0; - padding-bottom: 0; - line-height: 0; + margin: 4px 8px 4px 9px; + padding: 0; + line-height: 1.1; + position: relative; .ci-action-icon-container:hover { background-color: transparent; @@ -648,6 +680,11 @@ } } +.pipeline-graph .dropdown-build .ci-status-icon svg { + width: 18px; + height: 18px; +} + .ci-status-text { max-width: 110px; white-space: nowrap; @@ -656,7 +693,7 @@ vertical-align: bottom; display: inline-block; position: relative; - font-weight: 100; + font-weight: 200; } // Action Icons @@ -693,7 +730,7 @@ color: $gl-text-color-light; .build-content { - padding: 3px 7px 6px; + padding: 4px 7px 8px; } .ci-action-icon-container { From fab2f071abc80ee777fea6968147dfb735f2551c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 22 Dec 2016 12:17:28 +0000 Subject: [PATCH 164/206] Fix dropdown content non appearing in MR view Add MR ID to Changelog entry --- app/assets/javascripts/dispatcher.js.es6 | 5 ----- app/assets/javascripts/merge_request_tabs.js.es6 | 4 ++++ app/assets/stylesheets/pages/pipelines.scss | 4 ++++ changelogs/unreleased/25961-spec-list-blank.yml | 4 ++++ 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25961-spec-list-blank.yml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 5245c5aa494..78f68a247a2 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -140,11 +140,6 @@ case 'projects:merge_requests:commits': new MergedButtons(); break; - case 'projects:merge_requests:pipelines': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - break; case "projects:merge_requests:diffs": new gl.Diff(); new ZenMode(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 42015a02477..860e7e066a0 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -259,6 +259,10 @@ gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); this.pipelinesLoaded = true; this.scrollToElement('#pipelines'); + + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); }, }); } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 697887dcbe5..241bf02bafd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -52,6 +52,10 @@ } } +.content-list.pipelines .table-holder { + min-height: 300px; +} + .pipeline-holder { width: 100%; overflow: auto; diff --git a/changelogs/unreleased/25961-spec-list-blank.yml b/changelogs/unreleased/25961-spec-list-blank.yml new file mode 100644 index 00000000000..835def027a7 --- /dev/null +++ b/changelogs/unreleased/25961-spec-list-blank.yml @@ -0,0 +1,4 @@ +--- +title: Fix Pipeline builds list blank on MR +merge_request: 8255 +author: From b552f2b02039de374c9eca229ec2b220b1577c08 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 22 Dec 2016 12:31:25 +0000 Subject: [PATCH 165/206] Removed color-label overwrites --- app/assets/stylesheets/pages/milestone.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 77c523d7310..f47ae9c6157 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -25,12 +25,6 @@ } .issuable-row { - .color-label { - border-radius: 2px; - padding: 3px !important; - margin-right: 7px; - } - span a { color: $gl-text-color; word-wrap: break-word; From f6fceb653958f0f1b0c832a3e642ea7cea1fab24 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 22 Dec 2016 13:43:20 +0100 Subject: [PATCH 166/206] Fix Mattermost command creation by specifying username --- .../project_services/mattermost_slash_commands_service.rb | 2 +- .../project_services/mattermost_slash_commands_service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 6c78c0af71c..2cb481182d7 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -46,6 +46,6 @@ class MattermostSlashCommandsService < ChatSlashCommandsService description: "Perform common operations on: #{pretty_project_name}", display_name: "GitLab / #{pretty_project_name}", method: 'P', - user_name: 'GitLab') + username: 'GitLab') end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index d6f4fbd7265..672ced68681 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -36,7 +36,7 @@ describe MattermostSlashCommandsService, :models do description: "Perform common operations on: #{project.name_with_namespace}", display_name: "GitLab / #{project.name_with_namespace}", method: 'P', - user_name: 'GitLab' }.to_json). + username: 'GitLab' }.to_json). to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, From 0ad88cb8d35201260da2986782f10956abe4fc3a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 22 Dec 2016 12:48:14 +0000 Subject: [PATCH 167/206] Increase left padding of filter row labels --- app/assets/stylesheets/pages/labels.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d129eb12a45..237869aa544 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -98,7 +98,7 @@ } .label { - padding: 8px 9px 9px; + padding: 8px 9px 9px $gl-padding; font-size: 14px; } } From d64d2a22755c7c10ad69b4bbbdf0b5ad7d3fd2a8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 22 Dec 2016 13:50:26 +0100 Subject: [PATCH 168/206] Add changelog [ci skip] --- changelogs/unreleased/fix-mattermost-username.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-mattermost-username.yml diff --git a/changelogs/unreleased/fix-mattermost-username.yml b/changelogs/unreleased/fix-mattermost-username.yml new file mode 100644 index 00000000000..ca298e4d008 --- /dev/null +++ b/changelogs/unreleased/fix-mattermost-username.yml @@ -0,0 +1,4 @@ +--- +title: Fix Mattermost command creation by specifying username +merge_request: +author: From daff64452ff79b95eda5eb33c5aea8aface98f2b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 22 Dec 2016 14:02:45 +0100 Subject: [PATCH 169/206] Do not show retried builds in pipeline stage dropdown --- app/views/projects/pipelines/_stage.html.haml | 2 +- .../pipelines/_stage.html.haml_spec.rb | 46 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index 20456e792e7..cf1b366bf2c 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,4 +1,4 @@ %ul - - @stage.statuses.each do |status| + - @stage.statuses.latest.each do |status| %li.dropdown-build = render 'ci/status/graph_badge', subject: status diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb index eb7f7ca4a1a..d25de8af5d2 100644 --- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -7,15 +7,47 @@ describe 'projects/pipelines/_stage', :view do before do assign :stage, stage - - create(:ci_build, name: 'test:build', - stage: stage.name, - pipeline: pipeline) end - it 'shows the builds in the stage' do - render + context 'when there are only latest builds present' do + before do + create(:ci_build, name: 'test:build', + stage: stage.name, + pipeline: pipeline) + end - expect(rendered).to have_text 'test:build' + it 'shows the builds in the stage' do + render + + expect(rendered).to have_text 'test:build' + end + end + + context 'when build belongs to different stage' do + before do + create(:ci_build, name: 'test:build', + stage: 'other:stage', + pipeline: pipeline) + end + + it 'does not render build' do + render + + expect(rendered).not_to have_text 'test:build' + end + end + + context 'when there are retried builds present' do + before do + create_list(:ci_build, 2, name: 'test:build', + stage: stage.name, + pipeline: pipeline) + end + + it 'shows only latest builds' do + render + + expect(rendered).to have_text 'test:build', count: 1 + end end end From 3f9095722c0d78524e9e9bd4b3c343ace3d09f31 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 22 Dec 2016 14:09:46 +0100 Subject: [PATCH 170/206] Add Changelog entry for pipeline stage dropdown fix --- .../fix-hide-retried-builds-in-pipeline-stage-dropdown.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml diff --git a/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml b/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml new file mode 100644 index 00000000000..66256d7ed0e --- /dev/null +++ b/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Do not show retried builds in pipeline stage dropdown +merge_request: 8260 +author: From e46f2c53a282367581c5e4cb611a60dc20a59133 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Dec 2016 14:16:31 +0100 Subject: [PATCH 171/206] Don't render inline math when dollar signs are inside markup --- lib/banzai/filter/math_filter.rb | 11 +++-------- spec/lib/banzai/filter/math_filter_spec.rb | 7 +++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb index cb037f89337..b6e784c886b 100644 --- a/lib/banzai/filter/math_filter.rb +++ b/lib/banzai/filter/math_filter.rb @@ -5,12 +5,6 @@ module Banzai # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. # class MathFilter < HTML::Pipeline::Filter - # This picks out .... - INLINE_MATH = 'descendant-or-self::code'.freeze - - # Pick out a code block which is declared math - DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze - # Attribute indicating inline or display math. STYLE_ATTRIBUTE = 'data-math-style'.freeze @@ -22,13 +16,14 @@ module Banzai DOLLAR_SIGN = '$'.freeze def call - doc.xpath(INLINE_MATH).each do |code| + doc.css('code').each do |code| closing = code.next opening = code.previous # We need a sibling before and after. # They should end and start with $ respectively. if closing && opening && + closing.text? && opening.text? && closing.content.first == DOLLAR_SIGN && opening.content.last == DOLLAR_SIGN @@ -39,7 +34,7 @@ module Banzai end end - doc.xpath(DISPLAY_MATH).each do |el| + doc.css('pre.code.math').each do |el| el[STYLE_ATTRIBUTE] = 'display' el[:class] += " #{TAG_CLASS}" end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index 3fe2c7f5d5d..51883782e19 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -79,6 +79,13 @@ describe Banzai::Filter::MathFilter, lib: true do expect(doc.to_s).to eq input end + it 'ignores dollar signs if they are inside another element' do + input = '

We check strictly $2+2$

' + doc = filter(input) + + expect(doc.to_s).to eq input + end + # Display math it 'adds data-math-style display attribute to display math' do From c724126604f3592a7e8e3d121a7d27ecdfb760b4 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 22 Dec 2016 14:17:10 +0100 Subject: [PATCH 172/206] Post username as property, not user_name --- .../project_services/mattermost_slash_commands_service.rb | 2 +- .../project_services/mattermost_slash_commands_service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 6c78c0af71c..2cb481182d7 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -46,6 +46,6 @@ class MattermostSlashCommandsService < ChatSlashCommandsService description: "Perform common operations on: #{pretty_project_name}", display_name: "GitLab / #{pretty_project_name}", method: 'P', - user_name: 'GitLab') + username: 'GitLab') end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index d6f4fbd7265..672ced68681 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -36,7 +36,7 @@ describe MattermostSlashCommandsService, :models do description: "Perform common operations on: #{project.name_with_namespace}", display_name: "GitLab / #{project.name_with_namespace}", method: 'P', - user_name: 'GitLab' }.to_json). + username: 'GitLab' }.to_json). to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, From 4b7395e0fab8e35436957e41929b44f948a1c41a Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 22 Dec 2016 10:54:35 +0100 Subject: [PATCH 173/206] Fix format of Slack when result is nil --- app/models/project_services/slack_slash_commands_service.rb | 2 +- ...ommands_service.rb => slack_slash_commands_service_spec.rb} | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename spec/models/project_services/{slack_slash_commands_service.rb => slack_slash_commands_service_spec.rb} (93%) diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index cb19ebf4cad..5a7cc0fb329 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -16,7 +16,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService def trigger(params) # Format messages to be Slack-compatible super.tap do |result| - result[:text] = format(result[:text]) + result[:text] = format(result[:text]) if result.is_a?(Hash) end end diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb similarity index 93% rename from spec/models/project_services/slack_slash_commands_service.rb rename to spec/models/project_services/slack_slash_commands_service_spec.rb index 5775e439906..5766aa340e2 100644 --- a/spec/models/project_services/slack_slash_commands_service.rb +++ b/spec/models/project_services/slack_slash_commands_service_spec.rb @@ -18,7 +18,8 @@ describe SlackSlashCommandsService, :models do let(:service) do project.create_slack_slash_commands_service( - properties: { token: 'token' } + properties: { token: 'token' }, + active: true ) end From 22bbb24f28718849934693b8163155ccae4c4b5e Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 22 Dec 2016 13:27:39 +0000 Subject: [PATCH 174/206] Fix viewing "build failed" TODOs --- app/helpers/todos_helper.rb | 2 +- spec/factories/todos.rb | 1 + spec/features/todos/todos_spec.rb | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 09c69786791..74de25acf9d 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -35,7 +35,7 @@ module TodosHelper else path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] - path.unshift(:builds) if todo.build_failed? + path.unshift(:pipelines) if todo.build_failed? polymorphic_path(path, anchor: anchor) end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 866e663f026..082b02116c0 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -21,6 +21,7 @@ FactoryGirl.define do trait :build_failed do action { Todo::BUILD_FAILED } + target factory: :merge_request end trait :approval_required do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 88eabea7e3a..4bda0927692 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -155,5 +155,24 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.todos-all-done', count: 1) end end + + context 'User has a Build Failed todo' do + let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } + + before do + login_as user + visit dashboard_todos_path + end + + it 'shows the todo' do + expect(page).to have_content 'The build failed for your merge request' + end + + it 'links to the pipelines for the merge request' do + href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target) + + expect(page).to have_link "merge request #{todo.target.to_reference}", href + end + end end end From 328be22f4d9f611852db088d8a3de6c605920717 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Thu, 22 Dec 2016 15:47:06 +0100 Subject: [PATCH 175/206] Add Gitaly to the architecture of the application Added both to the graph, and to the high level overview. Explaining what is the goal of the system and how does it connect to the other systems. --- doc/development/architecture.md | 14 +++++++++----- .../gitlab_architecture_diagram.png | Bin 20339 -> 61944 bytes 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 33fd50f4c11..4eb7a8eee48 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -6,7 +6,7 @@ There are two editions of GitLab: [Enterprise Edition](https://about.gitlab.com/ EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/subscribers/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme. -Both EE and CE require an add-on component called gitlab-shell. It is obtained from the [gitlab-shell repository](https://gitlab.com/gitlab-org/gitlab-shell/tree/master). New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. +Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. ## Physical office analogy @@ -35,8 +35,10 @@ Their job description: - make tasks for Sidekiq; - fetch stuff from the warehouse or move things around in there; -**Gitlab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP). -Gitlab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk. +**GitLab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP). +GitLab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk. + +**Gitaly** is a back desk that is specialized on reaching the disks to perform git operations efficiently and keep a copy of the result of costly operations. All git operations go through Gitaly. **GitLab Enterprise Edition (the application)** is the collection of processes and business practices that the office is run by. @@ -53,7 +55,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell ### Components ![GitLab Diagram Overview](gitlab_architecture_diagram.png) - + _[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_ A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. @@ -62,7 +64,9 @@ The GitLab web app uses MySQL or PostgreSQL for persistent database information When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects. -The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories directly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. +The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. + +Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files) ### Installation Folder Summary diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png index cda5ce254ce478b33c9848894bcbd55806ed2281..ec1eaab179a0a6cf8c4c5694c160b603b1ff7d8e 100644 GIT binary patch literal 61944 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 O29qlmhGmy;hyNETJP^tN literal 20339 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 L2qe53<{17j#{XMU From 7241246a04773805397c0eefd44a6fba20f04a1a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 22 Dec 2016 16:00:49 +0000 Subject: [PATCH 176/206] Stops GFM special characters interfering with markdown tags --- app/assets/javascripts/gfm_auto_complete.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index cbd8ac4eddd..12875eaa1c3 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -74,15 +74,16 @@ // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - var _a, _y, regexp, match, atSymbols; - atSymbols = Object.keys(this.app.controllers).join('|'); + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); subtext = subtext.split(' ').pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); From ff28c149822503a2fdca4dfd3b0d4defaea28dc3 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Thu, 22 Dec 2016 17:21:10 +0100 Subject: [PATCH 177/206] Add changelog entry --- changelogs/unreleased/pc-add-gitaly-to-architecture.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/pc-add-gitaly-to-architecture.yml diff --git a/changelogs/unreleased/pc-add-gitaly-to-architecture.yml b/changelogs/unreleased/pc-add-gitaly-to-architecture.yml new file mode 100644 index 00000000000..7c18da698df --- /dev/null +++ b/changelogs/unreleased/pc-add-gitaly-to-architecture.yml @@ -0,0 +1,4 @@ +--- +title: Add Gitaly to the architecture documentation +merge_request: 8264 +author: Pablo Carranza From d7d0eb0db673d955ad7c51c699a63763cc831978 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 22 Dec 2016 14:47:10 -0200 Subject: [PATCH 178/206] Update CHANGELOG.md for 8.15.0 [ci skip] --- CHANGELOG.md | 207 ++++++++++++++++++ .../15081-wrong-login-tab-ldap-frontend.yml | 4 - .../18435-autocomplete-is-not-performant.yml | 4 - .../18546-update-wiki-page-design.yml | 4 - ...19550-fix-contributer-graph-duplicates.yml | 4 - .../unreleased/19620-auto-scroll-log.yml | 4 - .../19703-direct-link-pipelines.yml | 4 - .../20052-actions-table-vscroll.yml | 4 - .../unreleased/20492-access-token-scopes.yml | 4 - .../unreleased/22348-gitea-importer.yml | 4 - ...ce-queries-in-api-helpers-find_project.yml | 4 - .../unreleased/22604-manual-actions.yml | 4 - ...e-task-similar-to-gitlab-shell-install.yml | 4 - .../22742-filter-protocol-relative-urls.yml | 4 - .../22781-user-generated-permalinks.yml | 4 - .../unreleased/22849-ci-build-ref-slug.yml | 4 - .../unreleased/22864-add-environment-slug.yml | 4 - .../unreleased/22864-kubernetes-service.yml | 4 - .../unreleased/23500-enable-colorvariable.yml | 4 - ...-for-describe-pagination-params-in-api.yml | 4 - ...-sort-functionality-for-project-member.yml | 4 - .../unreleased/23589-open-issue-for-mr.yml | 5 - .../unreleased/23638-remove-builds-tab.yml | 4 - .../23718-backup-rake-task-human-readable.yml | 4 - ...-below-new-group-on-the-welcome-screen.yml | 4 - .../24150-consistent-dropdown-styles.yml | 4 - .../24224-fix-project-ref-cache.yml | 4 - ...bar-subscribe-button-style-improvement.yml | 4 - .../24413-show-unconfirmed-email-status.yml | 4 - ...e_deleted_branch_link_in_merge_request.yml | 4 - .../24576_cant_stop_impersonating.yml | 4 - ...10-fix-generic-commit-status-table-row.yml | 4 - .../unreleased/24726-remove-across-gitlab.yml | 4 - ...3-archived-project-merge-request-count.yml | 4 - .../24803-change-cursor-for-ca-stages.yml | 5 - .../24807-stop-ddosing-ourselves.yml | 4 - .../unreleased/24824-dropdown-items-focus.yml | 4 - .../unreleased/24844-environments-date.yml | 4 - ...nly-keeps-common-labels-when-searching.yml | 4 - ...dd-ssh-key-if-ssh-protocol-is-disabled.yml | 4 - .../24927-custom-event-polyfill-test.yml | 4 - ...merge-request-diff-containing-an-image.yml | 4 - ...ux-improvement-sign-in-success-message.yml | 5 - .../24999-fix-project-avatar-alignment.yml | 4 - .../25002-sentence-case-dashboard-tabs.yml | 4 - ...-collapsed-issue-merge-request-sidebar.yml | 4 - ...5026-authenticate-user-for-new-snippet.yml | 4 - ...031-do-not-raise-error-in-autocomplete.yml | 4 - ...3-hide-new-issue-btn-non-loggedin-user.yml | 4 - ...98-header-margins-on-pipeline-settings.yml | 5 - ...-hide-issue-mr-button-for-not-loggedin.yml | 4 - .../unreleased/25136-last-deployment-link.yml | 4 - ...ssue-create-needs-better-documentation.yml | 4 - .../25202-fix-mr-widget-content-wrapping.yml | 5 - .../25207-text-overflow-env-table.yml | 4 - ...25221-fix-build-status-overflow-mobile.yml | 4 - ...undefined-method-text-for-nil-nilclass.yml | 5 - changelogs/unreleased/25264-ref-commit.yml | 4 - .../25272_fix_comments_tab_disappearing.yml | 4 - .../25294-remove-signed-out-msg.yml | 4 - .../25301-git-2-11-force-push-bug.yml | 4 - ...324-change-housekeeping-btn-to-default.yml | 4 - ...ks-fired-for-issue-closed-and-reopened.yml | 4 - .../25368-fix-left-align-system-note.yml | 4 - changelogs/unreleased/25374-svg-as-prop.yml | 4 - changelogs/unreleased/25482-fix-api-sudo.yml | 4 - changelogs/unreleased/25483-broken-tabs.yml | 4 - ...adding-a-way-to-go-back-on-error-pages.yml | 4 - .../25617-todos-filter-placeholder.yml | 4 - .../25740-fix-new-branch-button-padding.yml | 4 - ...project-alerts-and-flash-notifications.yml | 4 - .../25895-fix-headers-in-ci-api-helpers.yml | 4 - .../25908-fix-grape-after-update.yml | 4 - .../unreleased/25938-progress-bar-gone.yml | 4 - changelogs/unreleased/4269-public-api.yml | 4 - .../unreleased/4269-public-files-api.yml | 4 - .../4269-public-repositories-api.yml | 4 - ...749-add-setting-to-disable-html-emails.yml | 3 - changelogs/unreleased/8003-katex-math.yml | 4 - .../8038-authentiq-id-oauth-support.yml | 4 - .../unreleased/abuse_report-fixture.yml | 4 - changelogs/unreleased/adam-auto-deploy.yml | 4 - changelogs/unreleased/add_info_to_qr.yml | 4 - .../unreleased/allow-more-filenames.yml | 4 - changelogs/unreleased/api-branch-status.yml | 4 - changelogs/unreleased/api-cherry-pick.yml | 4 - .../unreleased/api-delete-group-share.yml | 4 - .../api-expose-commiter-details.yml | 4 - .../unreleased/api-remove-source-branch.yml | 4 - .../unreleased/api-simple-group-project.yml | 4 - changelogs/unreleased/awards_handler.yml | 4 - changelogs/unreleased/bitbucket-oauth2.yml | 4 - .../cache-last-commit-sha-for-path.yml | 4 - .../change_development_build_fixtures.yml | 4 - .../unreleased/chomp-git-status-message.yml | 5 - .../unreleased/cleanup-common_utils-js.yml | 4 - changelogs/unreleased/comments-fixture.yml | 4 - .../create-dynamic-fixture-for-build_spec.yml | 4 - changelogs/unreleased/destroy-session.yml | 4 - changelogs/unreleased/dev-issue-24554.yml | 4 - ...h-main-when-fork-target-branch-updated.yml | 4 - .../unreleased/dockerfile-templates.yml | 4 - .../dz-allow-nested-group-routing.yml | 4 - changelogs/unreleased/dz-fix-route-rename.yml | 4 - changelogs/unreleased/dz-nested-groups.yml | 4 - .../unreleased/dz-rename-invalid-groups.yml | 4 - .../dz-whitelist-dashboard-project-path.yml | 4 - .../dz-whitelist-more-project-names-2.yml | 4 - .../dz-whitelist-more-project-names.yml | 4 - changelogs/unreleased/emoji-btn-disabled.yml | 4 - .../enable-asciidoctor-admonition-icons.yml | 4 - .../expose-deployment-variables.yml | 4 - .../feature-admin-user-groups-link.yml | 4 - .../unreleased/features-api-snippets.yml | 4 - ...ile-template-dropwdown-proper-position.yml | 4 - .../unreleased/fix-cancelling-pipelines.yml | 4 - ...te-pipeline-with-builds-in-transaction.yml | 4 - .../fix-drop-project-authorized-for-user.yml | 4 - .../fix-import-export-build-token.yml | 4 - .../fix-import-export-ee-services.yml | 4 - .../unreleased/fix-import-export-mr-error.yml | 4 - .../unreleased/fix-import-labels-error.yml | 4 - ...e-mwbs-to-merge-when-pipeline-succeeds.yml | 4 - .../fix-slack-pipeline-message-by-api.yml | 4 - changelogs/unreleased/fix-yaml-variables.yml | 4 - changelogs/unreleased/gem-update-grape.yml | 4 - .../unreleased/gitlab-workhorse-multipart.yml | 4 - .../unreleased/glm-shorthand-reference.yml | 4 - ...-add-diff-hunks-to-notification-emails.yml | 4 - .../html-safe-diff-line-content.yml | 4 - .../unreleased/improve-invite-accept-page.yml | 4 - .../issuable_filters_present-refactor.yml | 4 - changelogs/unreleased/issue-24534.yml | 4 - .../issue-boards-scrollable-element.yml | 4 - changelogs/unreleased/issue-events-filter.yml | 4 - changelogs/unreleased/issue_13270.yml | 4 - changelogs/unreleased/issue_22269.yml | 4 - changelogs/unreleased/issue_24363.yml | 4 - changelogs/unreleased/issue_24748.yml | 4 - changelogs/unreleased/issue_24958.yml | 4 - changelogs/unreleased/issues-1608-text.yml | 4 - changelogs/unreleased/jej-22869.yml | 4 - ...-use-mr-finder-instead-of-access-check.yml | 4 - ...7-move-issue-visible_to_user-to-finder.yml | 4 - ...jej-fix-missing-access-check-on-issues.yml | 4 - .../jej-fix-n-1-queries-milestones-show.yml | 4 - ...j-memoize-milestoneish-visible-to-user.yml | 4 - .../jej-note-search-uses-finder.yml | 4 - ...ssuable-finder-instead-of-access-check.yml | 4 - changelogs/unreleased/leave-project-btn.yml | 4 - .../mattermost-slash-auto-config.yml | 4 - changelogs/unreleased/members-dropdowns.yml | 4 - .../unreleased/milestone_start_date.yml | 4 - ...ove-abuse-report-spinach-test-to-rspec.yml | 4 - ...min-abuse-report-spinach-test-to-rspec.yml | 4 - ...dmin-active-tab-spinach-tests-to-rspec.yml | 4 - ...move-admin-hooks-spinach-test-to-rspec.yml | 4 - .../move-admin-logs-spinach-test-to-rspec.yml | 4 - .../move-admin-spam-spinach-test-to-rspec.yml | 4 - changelogs/unreleased/mr-origin-7855.yml | 4 - ...-22864-kubernetes-deploy-with-terminal.yml | 4 - .../unreleased/pipeline-build-hitbox.yml | 4 - .../unreleased/post_receive-any-email.yml | 4 - ...ocess-commit-worker-migration-encoding.yml | 4 - changelogs/unreleased/public-tags-api.yml | 4 - changelogs/unreleased/readme-link-fix.yml | 4 - .../refactor-create-service-spec.yml | 4 - .../unreleased/remove-backup-strategies.yml | 4 - .../remove-jsx-react-eslint-plugins.yml | 5 - .../remove-require-from-services.yml | 4 - .../unreleased/remove-u2f-error-logging.yml | 4 - ...ove-unnecessary-message-mr-commits-tab.yml | 4 - ...emove-unnecessary-self-from-user-model.yml | 4 - .../removing_unnecessary_indexes.yml | 4 - .../render-svg-in-diffs-and-notes.yml | 4 - .../unreleased/right-sidebar-fixture.yml | 4 - .../unreleased/rounded-labels-fixes.yml | 4 - .../unreleased/rs-project-team-helpers.yml | 4 - changelogs/unreleased/seed-runner-token.yml | 5 - .../unreleased/shortcuts-issuable-fixture.yml | 4 - ...how-commit-status-from-latest-pipeline.yml | 4 - .../simplify-create-new-list-issue-boards.yml | 4 - .../unreleased/small-emoji-adjustments.yml | 4 - .../unreleased/update-api-spec-files.yml | 4 - .../unreleased/update-button-font-weight.yml | 4 - .../unreleased/update-git-version-in-doc.yml | 4 - .../use-st-commits-where-possible.yml | 5 - .../unreleased/username-exists-root.yml | 4 - ...e-state-param-when-filtering-issuables.yml | 4 - changelogs/unreleased/zen-mode-fixture.yml | 4 - .../zj-expose-coverage-pipelines.yml | 4 - .../zj-fix-label-creation-non-members.yml | 4 - .../zj-guest-reads-public-builds.yml | 4 - .../zj-issue-new-over-issue-create.yml | 4 - .../zj-issue-search-slash-command.yml | 4 - .../unreleased/zj-remove-unused-services.yml | 4 - .../unreleased/zj-slack-slash-commands.yml | 4 - changelogs/unreleased/zj-use-ruby-2-3-3.yml | 4 - 198 files changed, 207 insertions(+), 797 deletions(-) delete mode 100644 changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml delete mode 100644 changelogs/unreleased/18435-autocomplete-is-not-performant.yml delete mode 100644 changelogs/unreleased/18546-update-wiki-page-design.yml delete mode 100644 changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml delete mode 100644 changelogs/unreleased/19620-auto-scroll-log.yml delete mode 100644 changelogs/unreleased/19703-direct-link-pipelines.yml delete mode 100644 changelogs/unreleased/20052-actions-table-vscroll.yml delete mode 100644 changelogs/unreleased/20492-access-token-scopes.yml delete mode 100644 changelogs/unreleased/22348-gitea-importer.yml delete mode 100644 changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml delete mode 100644 changelogs/unreleased/22604-manual-actions.yml delete mode 100644 changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml delete mode 100644 changelogs/unreleased/22742-filter-protocol-relative-urls.yml delete mode 100644 changelogs/unreleased/22781-user-generated-permalinks.yml delete mode 100644 changelogs/unreleased/22849-ci-build-ref-slug.yml delete mode 100644 changelogs/unreleased/22864-add-environment-slug.yml delete mode 100644 changelogs/unreleased/22864-kubernetes-service.yml delete mode 100644 changelogs/unreleased/23500-enable-colorvariable.yml delete mode 100644 changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml delete mode 100644 changelogs/unreleased/23573-sort-functionality-for-project-member.yml delete mode 100644 changelogs/unreleased/23589-open-issue-for-mr.yml delete mode 100644 changelogs/unreleased/23638-remove-builds-tab.yml delete mode 100644 changelogs/unreleased/23718-backup-rake-task-human-readable.yml delete mode 100644 changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml delete mode 100644 changelogs/unreleased/24150-consistent-dropdown-styles.yml delete mode 100644 changelogs/unreleased/24224-fix-project-ref-cache.yml delete mode 100644 changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml delete mode 100644 changelogs/unreleased/24413-show-unconfirmed-email-status.yml delete mode 100644 changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml delete mode 100644 changelogs/unreleased/24576_cant_stop_impersonating.yml delete mode 100644 changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml delete mode 100644 changelogs/unreleased/24726-remove-across-gitlab.yml delete mode 100644 changelogs/unreleased/24733-archived-project-merge-request-count.yml delete mode 100644 changelogs/unreleased/24803-change-cursor-for-ca-stages.yml delete mode 100644 changelogs/unreleased/24807-stop-ddosing-ourselves.yml delete mode 100644 changelogs/unreleased/24824-dropdown-items-focus.yml delete mode 100644 changelogs/unreleased/24844-environments-date.yml delete mode 100644 changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml delete mode 100644 changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml delete mode 100644 changelogs/unreleased/24927-custom-event-polyfill-test.yml delete mode 100644 changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml delete mode 100644 changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml delete mode 100644 changelogs/unreleased/24999-fix-project-avatar-alignment.yml delete mode 100644 changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml delete mode 100644 changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml delete mode 100644 changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml delete mode 100644 changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml delete mode 100644 changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml delete mode 100644 changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml delete mode 100644 changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml delete mode 100644 changelogs/unreleased/25136-last-deployment-link.yml delete mode 100644 changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml delete mode 100644 changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml delete mode 100644 changelogs/unreleased/25207-text-overflow-env-table.yml delete mode 100644 changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml delete mode 100644 changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml delete mode 100644 changelogs/unreleased/25264-ref-commit.yml delete mode 100644 changelogs/unreleased/25272_fix_comments_tab_disappearing.yml delete mode 100644 changelogs/unreleased/25294-remove-signed-out-msg.yml delete mode 100644 changelogs/unreleased/25301-git-2-11-force-push-bug.yml delete mode 100644 changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml delete mode 100644 changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml delete mode 100644 changelogs/unreleased/25368-fix-left-align-system-note.yml delete mode 100644 changelogs/unreleased/25374-svg-as-prop.yml delete mode 100644 changelogs/unreleased/25482-fix-api-sudo.yml delete mode 100644 changelogs/unreleased/25483-broken-tabs.yml delete mode 100644 changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml delete mode 100644 changelogs/unreleased/25617-todos-filter-placeholder.yml delete mode 100644 changelogs/unreleased/25740-fix-new-branch-button-padding.yml delete mode 100644 changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml delete mode 100644 changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml delete mode 100644 changelogs/unreleased/25908-fix-grape-after-update.yml delete mode 100644 changelogs/unreleased/25938-progress-bar-gone.yml delete mode 100644 changelogs/unreleased/4269-public-api.yml delete mode 100644 changelogs/unreleased/4269-public-files-api.yml delete mode 100644 changelogs/unreleased/4269-public-repositories-api.yml delete mode 100644 changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml delete mode 100644 changelogs/unreleased/8003-katex-math.yml delete mode 100644 changelogs/unreleased/8038-authentiq-id-oauth-support.yml delete mode 100644 changelogs/unreleased/abuse_report-fixture.yml delete mode 100644 changelogs/unreleased/adam-auto-deploy.yml delete mode 100644 changelogs/unreleased/add_info_to_qr.yml delete mode 100644 changelogs/unreleased/allow-more-filenames.yml delete mode 100644 changelogs/unreleased/api-branch-status.yml delete mode 100644 changelogs/unreleased/api-cherry-pick.yml delete mode 100644 changelogs/unreleased/api-delete-group-share.yml delete mode 100644 changelogs/unreleased/api-expose-commiter-details.yml delete mode 100644 changelogs/unreleased/api-remove-source-branch.yml delete mode 100644 changelogs/unreleased/api-simple-group-project.yml delete mode 100644 changelogs/unreleased/awards_handler.yml delete mode 100644 changelogs/unreleased/bitbucket-oauth2.yml delete mode 100644 changelogs/unreleased/cache-last-commit-sha-for-path.yml delete mode 100644 changelogs/unreleased/change_development_build_fixtures.yml delete mode 100644 changelogs/unreleased/chomp-git-status-message.yml delete mode 100644 changelogs/unreleased/cleanup-common_utils-js.yml delete mode 100644 changelogs/unreleased/comments-fixture.yml delete mode 100644 changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml delete mode 100644 changelogs/unreleased/destroy-session.yml delete mode 100644 changelogs/unreleased/dev-issue-24554.yml delete mode 100644 changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml delete mode 100644 changelogs/unreleased/dockerfile-templates.yml delete mode 100644 changelogs/unreleased/dz-allow-nested-group-routing.yml delete mode 100644 changelogs/unreleased/dz-fix-route-rename.yml delete mode 100644 changelogs/unreleased/dz-nested-groups.yml delete mode 100644 changelogs/unreleased/dz-rename-invalid-groups.yml delete mode 100644 changelogs/unreleased/dz-whitelist-dashboard-project-path.yml delete mode 100644 changelogs/unreleased/dz-whitelist-more-project-names-2.yml delete mode 100644 changelogs/unreleased/dz-whitelist-more-project-names.yml delete mode 100644 changelogs/unreleased/emoji-btn-disabled.yml delete mode 100644 changelogs/unreleased/enable-asciidoctor-admonition-icons.yml delete mode 100644 changelogs/unreleased/expose-deployment-variables.yml delete mode 100644 changelogs/unreleased/feature-admin-user-groups-link.yml delete mode 100644 changelogs/unreleased/features-api-snippets.yml delete mode 100644 changelogs/unreleased/file-template-dropwdown-proper-position.yml delete mode 100644 changelogs/unreleased/fix-cancelling-pipelines.yml delete mode 100644 changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml delete mode 100644 changelogs/unreleased/fix-drop-project-authorized-for-user.yml delete mode 100644 changelogs/unreleased/fix-import-export-build-token.yml delete mode 100644 changelogs/unreleased/fix-import-export-ee-services.yml delete mode 100644 changelogs/unreleased/fix-import-export-mr-error.yml delete mode 100644 changelogs/unreleased/fix-import-labels-error.yml delete mode 100644 changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml delete mode 100644 changelogs/unreleased/fix-slack-pipeline-message-by-api.yml delete mode 100644 changelogs/unreleased/fix-yaml-variables.yml delete mode 100644 changelogs/unreleased/gem-update-grape.yml delete mode 100644 changelogs/unreleased/gitlab-workhorse-multipart.yml delete mode 100644 changelogs/unreleased/glm-shorthand-reference.yml delete mode 100644 changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml delete mode 100644 changelogs/unreleased/html-safe-diff-line-content.yml delete mode 100644 changelogs/unreleased/improve-invite-accept-page.yml delete mode 100644 changelogs/unreleased/issuable_filters_present-refactor.yml delete mode 100644 changelogs/unreleased/issue-24534.yml delete mode 100644 changelogs/unreleased/issue-boards-scrollable-element.yml delete mode 100644 changelogs/unreleased/issue-events-filter.yml delete mode 100644 changelogs/unreleased/issue_13270.yml delete mode 100644 changelogs/unreleased/issue_22269.yml delete mode 100644 changelogs/unreleased/issue_24363.yml delete mode 100644 changelogs/unreleased/issue_24748.yml delete mode 100644 changelogs/unreleased/issue_24958.yml delete mode 100644 changelogs/unreleased/issues-1608-text.yml delete mode 100644 changelogs/unreleased/jej-22869.yml delete mode 100644 changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml delete mode 100644 changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml delete mode 100644 changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml delete mode 100644 changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml delete mode 100644 changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml delete mode 100644 changelogs/unreleased/jej-note-search-uses-finder.yml delete mode 100644 changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml delete mode 100644 changelogs/unreleased/leave-project-btn.yml delete mode 100644 changelogs/unreleased/mattermost-slash-auto-config.yml delete mode 100644 changelogs/unreleased/members-dropdowns.yml delete mode 100644 changelogs/unreleased/milestone_start_date.yml delete mode 100644 changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml delete mode 100644 changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml delete mode 100644 changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml delete mode 100644 changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml delete mode 100644 changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml delete mode 100644 changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml delete mode 100644 changelogs/unreleased/mr-origin-7855.yml delete mode 100644 changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml delete mode 100644 changelogs/unreleased/pipeline-build-hitbox.yml delete mode 100644 changelogs/unreleased/post_receive-any-email.yml delete mode 100644 changelogs/unreleased/process-commit-worker-migration-encoding.yml delete mode 100644 changelogs/unreleased/public-tags-api.yml delete mode 100644 changelogs/unreleased/readme-link-fix.yml delete mode 100644 changelogs/unreleased/refactor-create-service-spec.yml delete mode 100644 changelogs/unreleased/remove-backup-strategies.yml delete mode 100644 changelogs/unreleased/remove-jsx-react-eslint-plugins.yml delete mode 100644 changelogs/unreleased/remove-require-from-services.yml delete mode 100644 changelogs/unreleased/remove-u2f-error-logging.yml delete mode 100644 changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml delete mode 100644 changelogs/unreleased/remove-unnecessary-self-from-user-model.yml delete mode 100644 changelogs/unreleased/removing_unnecessary_indexes.yml delete mode 100644 changelogs/unreleased/render-svg-in-diffs-and-notes.yml delete mode 100644 changelogs/unreleased/right-sidebar-fixture.yml delete mode 100644 changelogs/unreleased/rounded-labels-fixes.yml delete mode 100644 changelogs/unreleased/rs-project-team-helpers.yml delete mode 100644 changelogs/unreleased/seed-runner-token.yml delete mode 100644 changelogs/unreleased/shortcuts-issuable-fixture.yml delete mode 100644 changelogs/unreleased/show-commit-status-from-latest-pipeline.yml delete mode 100644 changelogs/unreleased/simplify-create-new-list-issue-boards.yml delete mode 100644 changelogs/unreleased/small-emoji-adjustments.yml delete mode 100644 changelogs/unreleased/update-api-spec-files.yml delete mode 100644 changelogs/unreleased/update-button-font-weight.yml delete mode 100644 changelogs/unreleased/update-git-version-in-doc.yml delete mode 100644 changelogs/unreleased/use-st-commits-where-possible.yml delete mode 100644 changelogs/unreleased/username-exists-root.yml delete mode 100644 changelogs/unreleased/validate-state-param-when-filtering-issuables.yml delete mode 100644 changelogs/unreleased/zen-mode-fixture.yml delete mode 100644 changelogs/unreleased/zj-expose-coverage-pipelines.yml delete mode 100644 changelogs/unreleased/zj-fix-label-creation-non-members.yml delete mode 100644 changelogs/unreleased/zj-guest-reads-public-builds.yml delete mode 100644 changelogs/unreleased/zj-issue-new-over-issue-create.yml delete mode 100644 changelogs/unreleased/zj-issue-search-slash-command.yml delete mode 100644 changelogs/unreleased/zj-remove-unused-services.yml delete mode 100644 changelogs/unreleased/zj-slack-slash-commands.yml delete mode 100644 changelogs/unreleased/zj-use-ruby-2-3-3.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 786b0128aac..4fd40b82434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,213 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.15.0 (2017-01-22) + +- Whitelist next project names: notes, services. +- Use Grape's new Route methods. +- Fixed issue boards scrolling with a lot of lists & issues. +- Remove unnecessary sentences for status codes in the API documentation. (Luis Alonso Chavez Armendariz) +- Allow unauthenticated access to Repositories Files API GET endpoints. +- Add note to the invite page when the logged in user email is not the same as the invitation. +- Don't accidentally mark unsafe diff lines as HTML safe. +- Add git diff context to notifications of new notes on merge requests. (Heidi Hoopes) +- Shows group members in project members list. +- Gem update: Update grape to 0.18.0. (Robert Schilling) +- API: Expose merge status for branch API. (Robert Schilling) +- Displays milestone remaining days only when it's present. +- API: Expose committer details for commits. (Robert Schilling) +- API: Ability to set 'should_remove_source_branch' on merge requests. (Robert Schilling) +- Fix project import label priorities error. +- Fix Import/Export merge requests error while importing. +- Refactor Bitbucket importer to use BitBucket API Version 2. +- Fix Import/Export duplicated builds error. +- Ci::Builds have same ref as Ci::Pipeline in dev fixtures. (twonegatives) +- For single line git commit messages, the close quote should be on the same line as the open quote. +- Use authorized projects in ProjectTeam. +- Destroy a user's session when they delete their own account. +- Edit help text to clarify annotated tag creation. (Liz Lam) +- Fixed file template dropdown for the "New File" editor for smaller/zoomed screens. +- Fix Route#rename_children behavior. +- Add nested groups support on data level. +- Allow projects with 'dashboard' as path. +- Disabled emoji buttons when user is not logged in. +- Remove unused and void services from the database. +- Add issue search slash command. +- Accept issue new as command to create an issue. +- Non members cannot create labels through the API. +- API: expose pipeline coverage. +- Validate state param when filtering issuables. +- Username exists check respects relative root path. +- Bump Git version requirement to 2.8.4. +- Updates the font weight of button styles because of the change to system fonts. +- Update API spec files to describe the correct class. (Livier) +- Fixed timeago re-rendering every timeago. +- Enable ColorVariable in scss-lint. (Sam Rose) +- Various small emoji positioning adjustments. +- Add shortcuts for adding users to a project team with a specific role. (Nikolay Ponomarev and Dino M) +- Additional rounded label fixes. +- Remove unnecessary database indices. +- 24726 Remove Across GitLab from side navigation. +- Changed cursor icon to pointer when mousing over stages on the Cycle Analytics pages. (Ryan Harris) +- Add focus state to dropdown items. +- Fixes Environments displaying incorrect date since 8.14 upgrade. +- Improve bulk assignment for issuables. +- Stop supporting Google and Azure as backup strategies. +- Fix broken README.md UX guide link. +- Allow public access to some Tag API endpoints. +- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors. +- Adjust the width of project avatars to fix alignment within their container. (Ryan Harris) +- Sentence cased the nav tab headers on the project dashboard page. (Ryan Harris) +- Adds hoverstates for collapsed Issue/Merge Request sidebar. +- Make CI badge hitboxes match parent. +- Add a starting date to milestones. +- Adjusted margins for Build Status and Coverage Report rows to match those of the CI/CD Pipeline row. (Ryan Harris) +- Updated members dropdowns. +- Move all action buttons to project header. +- Replace issue access checks with use of IssuableFinder. +- Fix missing Note access checks by moving Note#search to updated NoteFinder. +- Centered Accept Merge Request button within MR widget and added padding for viewports smaller than 768px. (Ryan Harris) +- Fix missing access checks on issue lookup using IssuableFinder. +- Added top margin to Build status page header for mobile views. (Ryan Harris) +- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages. +- Issue#visible_to_user moved to IssuesFinder to prevent accidental use. +- Replace MR access checks with use of MergeRequestsFinder. +- Fix information disclosure in `Projects::BlobController#update`. +- Allow branch names with dots on API endpoint. +- Changed Housekeeping button on project settings page to default styling. (Ryan Harris) +- Ensure issuable state changes only fire webhooks once. +- Fix bad selection on dropdown menu for tags filter. (Luis Alonso Chavez Armendariz) +- Fix title case to sentence case. (Luis Alonso Chavez Armendariz) +- Fix appearance in error pages. (Luis Alonso Chavez Armendariz) +- Create mattermost service. +- 25617 Fix placeholder color of todo filters. +- Made the padding on the plus button in the breadcrumb menu even. (Ryan Harris) +- Allow to delete tag release note. +- Ensure nil User-Agent doesn't break the CI API. +- Replace Rack::Multipart with GitLab-Workhorse based solution. !5867 +- Add scopes for personal access tokens and OAuth tokens. !5951 +- API: Endpoint to expose personal snippets as /snippets. !6373 (Bernard Guyzmo Pratz) +- New `gitlab:workhorse:install` rake task. !6574 +- Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. !6635 (Makoto Scott-Hinkle) +- Add support for setting the GitLab Runners Registration Token during initial database seeding. !6642 +- Guests can read builds when public. !6842 +- Made comment autocomplete more performant and removed some loading bugs. !6856 +- Add GitLab host to 2FA QR code and manual info. !6941 +- Add sorting functionality for group/project members. !7032 +- Rename Merge When Build Succeeds to Merge When Pipeline Succeeds. !7135 +- Resolve all discussions in a merge request by creating an issue collecting them. !7180 (Bob Van Landuyt) +- Add Human Readable format for rake backup. !7188 (David Gerő) +- post_receive: accept any user email from last commit. !7225 (Elan Ruusamäe) +- Add support for Dockerfile templates. !7247 +- Add shorthand support to gitlab markdown references. !7255 (Oswaldo Ferreira) +- Display error code for U2F errors. !7305 (winniehell) +- Fix wrong tab selected when loggin fails and multiple login tabs exists. !7314 (Jacopo Beschi @jacopo-beschi) +- Clean up common_utils.js. !7318 (winniehell) +- Show commit status from latest pipeline. !7333 +- Remove the help text under the sidebar subscribe button and style it inline. !7389 +- Update wiki page design. !7429 +- Add nested groups support to the routing. !7459 +- Changed eslint airbnb config to the base airbnb config and corrected eslintrc plugins and envs. !7470 (Luke "Jared" Bennett) +- Fix cancelling created or external pipelines. !7508 +- Allow admins to stop impersonating users without e-mail addresses. !7550 (Oren Kanner) +- Remove unnecessary self from user model. !7551 (Semyon Pupkov) +- Homogenize filter and sort dropdown look'n'feel. !7583 (David Wagner) +- Create dynamic fixture for build_spec. !7589 (winniehell) +- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 +- Remove unnecessary require_relative calls from service classes. !7601 (Semyon Pupkov) +- Simplify copy on "Create a new list" dropdown in Issue Boards. !7605 (Victor Rodrigues) +- Refactor create service spec. !7609 (Semyon Pupkov) +- Shows unconfirmed email status in profile. !7611 +- The admin user projects view now has a clickable group link. !7620 (James Gregory) +- Prevent DOM ID collisions resulting from user-generated content anchors. !7631 +- Replace static fixture for abuse_reports_spec. !7644 (winniehell) +- Define common helper for describe pagination params in api. !7646 (Semyon Pupkov) +- Move abuse report spinach test to rspec. !7659 (Semyon Pupkov) +- Replace static fixture for awards_handler_spec. !7661 (winniehell) +- API: Add ability to unshare a project from a group. !7662 (Robert Schilling) +- Replace references to MergeRequestDiff#commits with st_commits when we care only about the number of commits. !7668 +- Add issue events filter and make all really show all events. !7673 (Oxan van Leeuwen) +- Replace static fixture for notes_spec. !7683 (winniehell) +- Replace static fixture for shortcuts_issuable_spec. !7685 (winniehell) +- Replace static fixture for zen_mode_spec. !7686 (winniehell) +- Replace static fixture for right_sidebar_spec. !7687 (winniehell) +- Add online terminal support for Kubernetes. !7690 +- Move admin abuse report spinach test to rspec. !7691 (Semyon Pupkov) +- Move admin spam spinach test to Rspec. !7708 (Semyon Pupkov) +- Make API::Helpers find a project with only one query. !7714 +- Create builds in transaction to avoid empty pipelines. !7742 +- Render SVG images in diffs and notes. !7747 (andrebsguedes) +- Add setting to enable/disable HTML emails. !7749 +- Use SmartInterval for MR widget and improve visibilitychange functionality. !7762 +- Resolve "Remove Builds tab from Merge Requests and Commits". !7763 +- Moved new projects button below new group button on the welcome screen. !7770 +- fix display hook error message. !7775 (basyura) +- Refactor issuable_filters_present to reduce duplications. !7776 (Semyon Pupkov) +- Redirect to sign-in page when unauthenticated user tries to create a snippet. !7786 +- Fix Archived project merge requests add to group's Merge Requests. !7790 (Jacopo Beschi @jacopo-beschi) +- Update generic/external build status to match normal build status template. !7811 +- Enable AsciiDoctor admonition icons. !7812 (Horacio Sanson) +- Do not raise error in AutocompleteController#users when not authorized. !7817 (Semyon Pupkov) +- fix: 24982- Remove'Signed in successfully' message After this change the sign-in-success flash message will not be shown. !7837 (jnoortheen) +- Fix Latest deployment link is broken. !7839 +- Don't display prompt to add SSH keys if SSH protocol is disabled. !7840 (Andrew Smith (EspadaV8)) +- Allow unauthenticated access to some Project API GET endpoints. !7843 +- Refactor presenters ChatCommands. !7846 +- Improve help message for issue create slash command. !7850 +- change text around timestamps to make it clear which timestamp is displayed. !7860 (BM5k) +- Improve Build Log scrolling experience. !7895 +- Change ref property to commitRef in vue commit component. !7901 +- Prevent user creating issue or MR without signing in for a group. !7902 +- Provides a sensible default message when adding a README to a project. !7903 +- Bump ruby version to 2.3.3. !7904 +- Fix comments activity tab visibility condition. !7913 (Rydkin Maxim) +- Remove unnecessary target branch link from MR page in case of deleted target branch. !7916 (Rydkin Maxim) +- Add image controls to MR diffs. !7919 +- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930 +- Resolve "Manual actions on pipeline graph". !7931 +- Avoid escaping relative links in Markdown twice. !7940 (winniehell) +- Move admin hooks spinach to rspec. !7942 (Semyon Pupkov) +- Move admin logs spinach test to rspec. !7945 (Semyon Pupkov) +- fix: removed signed_out notification. !7958 (jnoortheen) +- Accept environment variables from the `pre-receive` script. !7967 +- Do not reload diff for merge request made from fork when target branch in fork is updated. !7973 +- Fixes left align issue for long system notes. !7982 +- Add a slug to environments. !7983 +- Fix lookup of project by unknown ref when caching is enabled. !7988 +- Resolve "Provide SVG as a prop instead of hiding and copy them in environments table". !7992 +- Introduce deployment services, starting with a KubernetesService. !7994 +- Adds tests for custom event polyfill. !7996 +- Allow all alphanumeric characters in file names. !8002 (winniehell) +- Added support for math rendering, using KaTeX, in Markdown and asciidoc. !8003 (Munken) +- Remove unnecessary commits order message. !8004 +- API: Memoize the current_user so that sudo can work properly. !8017 +- group authors in contribution graph with case insensitive email handle comparison. !8021 +- Move admin active tab spinach tests to rspec. !8037 (Semyon Pupkov) +- Add Authentiq as Oauth provider. !8038 (Alexandros Keramidas) +- API: Ability to cherry pick a commit. !8047 (Robert Schilling) +- Fix Slack pipeline message from pipelines made by API. !8059 +- API: Simple representation of group's projects. !8060 (Robert Schilling) +- Prevent overflow with vertical scroll when we have space to show content. !8061 +- Allow to auto-configure Mattermost. !8070 +- Introduce $CI_BUILD_REF_SLUG. !8072 +- Added go back anchor on error pages. !8087 +- Convert CI YAML variables keys into strings. !8088 +- Adds Direct link from pipeline list to builds. !8097 +- Cache last commit id for path. !8098 (Hiroyuki Sato) +- Pass variables from deployment project services to CI runner. !8107 +- New Gitea importer. !8116 +- Introduce "Set up autodeploy" button to help configure GitLab CI for deployment. !8135 +- Prevent enviroment table to overflow when name has underscores. !8142 +- Fix missing service error importing from EE to CE. !8144 +- Milestoneish SQL performance partially improved and memoized. !8146 +- Allow unauthenticated access to Repositories API GET endpoints. !8148 +- fix colors and margins for adjacent alert banners. !8151 +- Hides new issue button for non loggedin user. !8175 +- Fix N+1 queries on milestone show pages. !8185 +- Rename groups with .git in the end of the path. !8199 +- Whitelist next project names: help, ci, admin, search. !8227 +- Adds back CSS for progress-bars. !8237 + ## 8.14.5 (2016-12-14) - Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml deleted file mode 100644 index 19c76b5b437..00000000000 --- a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong tab selected when loggin fails and multiple login tabs exists -merge_request: 7314 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml deleted file mode 100644 index 019c55e27dc..00000000000 --- a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Made comment autocomplete more performant and removed some loading bugs -merge_request: 6856 -author: diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml deleted file mode 100644 index c76e17340f2..00000000000 --- a/changelogs/unreleased/18546-update-wiki-page-design.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update wiki page design -merge_request: 7429 -author: diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml deleted file mode 100644 index 742b10e72aa..00000000000 --- a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: group authors in contribution graph with case insensitive email handle comparison -merge_request: 8021 -author: diff --git a/changelogs/unreleased/19620-auto-scroll-log.yml b/changelogs/unreleased/19620-auto-scroll-log.yml deleted file mode 100644 index cf38096683b..00000000000 --- a/changelogs/unreleased/19620-auto-scroll-log.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve Build Log scrolling experience -merge_request: 7895 -author: diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml deleted file mode 100644 index d846ad41e0f..00000000000 --- a/changelogs/unreleased/19703-direct-link-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds Direct link from pipeline list to builds -merge_request: 8097 -author: diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml deleted file mode 100644 index 779cd08de09..00000000000 --- a/changelogs/unreleased/20052-actions-table-vscroll.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent overflow with vertical scroll when we have space to show content -merge_request: 8061 -author: diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml deleted file mode 100644 index a9424ded662..00000000000 --- a/changelogs/unreleased/20492-access-token-scopes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add scopes for personal access tokens and OAuth tokens -merge_request: 5951 -author: diff --git a/changelogs/unreleased/22348-gitea-importer.yml b/changelogs/unreleased/22348-gitea-importer.yml deleted file mode 100644 index 2aeefb0b259..00000000000 --- a/changelogs/unreleased/22348-gitea-importer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: New Gitea importer -merge_request: 8116 -author: diff --git a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml deleted file mode 100644 index 7f1d40e7c21..00000000000 --- a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Make API::Helpers find a project with only one query' -merge_request: 7714 -author: diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml deleted file mode 100644 index 7335e597292..00000000000 --- a/changelogs/unreleased/22604-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve "Manual actions on pipeline graph" -merge_request: 7931 -author: diff --git a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml deleted file mode 100644 index 54bd313f075..00000000000 --- a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: New `gitlab:workhorse:install` rake task -merge_request: 6574 -author: diff --git a/changelogs/unreleased/22742-filter-protocol-relative-urls.yml b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml deleted file mode 100644 index b331f5a4eb5..00000000000 --- a/changelogs/unreleased/22742-filter-protocol-relative-urls.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742' -merge_request: 6635 -author: Makoto Scott-Hinkle diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml deleted file mode 100644 index e46739e48e3..00000000000 --- a/changelogs/unreleased/22781-user-generated-permalinks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent DOM ID collisions resulting from user-generated content anchors -merge_request: 7631 -author: diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml deleted file mode 100644 index b159ecca6d8..00000000000 --- a/changelogs/unreleased/22849-ci-build-ref-slug.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce $CI_BUILD_REF_SLUG -merge_request: 8072 -author: diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml deleted file mode 100644 index f90f79337d5..00000000000 --- a/changelogs/unreleased/22864-add-environment-slug.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a slug to environments -merge_request: 7983 -author: diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml deleted file mode 100644 index ea1323cbeb0..00000000000 --- a/changelogs/unreleased/22864-kubernetes-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce deployment services, starting with a KubernetesService -merge_request: 7994 -author: diff --git a/changelogs/unreleased/23500-enable-colorvariable.yml b/changelogs/unreleased/23500-enable-colorvariable.yml deleted file mode 100644 index 98e22a934b8..00000000000 --- a/changelogs/unreleased/23500-enable-colorvariable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable ColorVariable in scss-lint -merge_request: -author: Sam Rose diff --git a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml deleted file mode 100644 index bb9e96d7581..00000000000 --- a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Define common helper for describe pagination params in api -merge_request: 7646 -author: Semyon Pupkov diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml deleted file mode 100644 index 73de0a6351b..00000000000 --- a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add sorting functionality for group/project members -merge_request: 7032 -author: diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml deleted file mode 100644 index cea48b85254..00000000000 --- a/changelogs/unreleased/23589-open-issue-for-mr.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve all discussions in a merge request by creating an issue collecting - them -merge_request: 7180 -author: Bob Van Landuyt diff --git a/changelogs/unreleased/23638-remove-builds-tab.yml b/changelogs/unreleased/23638-remove-builds-tab.yml deleted file mode 100644 index 86d63208761..00000000000 --- a/changelogs/unreleased/23638-remove-builds-tab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve "Remove Builds tab from Merge Requests and Commits" -merge_request: 7763 -author: diff --git a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml deleted file mode 100644 index 2e7583244ac..00000000000 --- a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Human Readable format for rake backup -merge_request: 7188 -author: David Gerő diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml deleted file mode 100644 index 855e4e1ba1d..00000000000 --- a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Moved new projects button below new group button on the welcome screen -merge_request: 7770 -author: diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml deleted file mode 100644 index a328d796c43..00000000000 --- a/changelogs/unreleased/24150-consistent-dropdown-styles.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Homogenize filter and sort dropdown look'n'feel -merge_request: 7583 -author: David Wagner diff --git a/changelogs/unreleased/24224-fix-project-ref-cache.yml b/changelogs/unreleased/24224-fix-project-ref-cache.yml deleted file mode 100644 index a6824ee44de..00000000000 --- a/changelogs/unreleased/24224-fix-project-ref-cache.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix lookup of project by unknown ref when caching is enabled -merge_request: 7988 -author: diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml deleted file mode 100644 index 2227c81bd34..00000000000 --- a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove the help text under the sidebar subscribe button and style it inline -merge_request: 7389 -author: diff --git a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml deleted file mode 100644 index 972eaed95e0..00000000000 --- a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Shows unconfirmed email status in profile -merge_request: 7611 -author: diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml deleted file mode 100644 index 34999480d4a..00000000000 --- a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Remove unnecessary target branch link from MR page in case of deleted target branch' -merge_request: 7916 -author: Rydkin Maxim diff --git a/changelogs/unreleased/24576_cant_stop_impersonating.yml b/changelogs/unreleased/24576_cant_stop_impersonating.yml deleted file mode 100644 index 8fa6eeca756..00000000000 --- a/changelogs/unreleased/24576_cant_stop_impersonating.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow admins to stop impersonating users without e-mail addresses -merge_request: 7550 -author: Oren Kanner diff --git a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml deleted file mode 100644 index 07cb53d5278..00000000000 --- a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update generic/external build status to match normal build status template -merge_request: 7811 -author: diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml deleted file mode 100644 index 6436e4b688f..00000000000 --- a/changelogs/unreleased/24726-remove-across-gitlab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 24726 Remove Across GitLab from side navigation -merge_request: -author: diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml deleted file mode 100644 index 2bc7e91825a..00000000000 --- a/changelogs/unreleased/24733-archived-project-merge-request-count.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Archived project merge requests add to group's Merge Requests -merge_request: 7790 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml deleted file mode 100644 index b9d84c0ce31..00000000000 --- a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics - pages -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml deleted file mode 100644 index 49e6c5e56e5..00000000000 --- a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use SmartInterval for MR widget and improve visibilitychange functionality -merge_request: 7762 -author: diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml deleted file mode 100644 index 66970c2a9a5..00000000000 --- a/changelogs/unreleased/24824-dropdown-items-focus.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add focus state to dropdown items -merge_request: -author: diff --git a/changelogs/unreleased/24844-environments-date.yml b/changelogs/unreleased/24844-environments-date.yml deleted file mode 100644 index 2bc23d40a68..00000000000 --- a/changelogs/unreleased/24844-environments-date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes Environments displaying incorrect date since 8.14 upgrade -merge_request: -author: diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml deleted file mode 100644 index cc7c2604824..00000000000 --- a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve bulk assignment for issuables -merge_request: -author: diff --git a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml deleted file mode 100644 index 4d4019e770e..00000000000 --- a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't display prompt to add SSH keys if SSH protocol is disabled -merge_request: 7840 -author: Andrew Smith (EspadaV8) diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml deleted file mode 100644 index 879c28a951e..00000000000 --- a/changelogs/unreleased/24927-custom-event-polyfill-test.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds tests for custom event polyfill -merge_request: 7996 -author: diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml deleted file mode 100644 index b8ba9391530..00000000000 --- a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add image controls to MR diffs -merge_request: 7919 -author: diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml deleted file mode 100644 index 12ea08e3815..00000000000 --- a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'fix: 24982- Remove''Signed in successfully'' message After this change the - sign-in-success flash message will not be shown' -merge_request: 7837 -author: jnoortheen diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml deleted file mode 100644 index 7af812e7359..00000000000 --- a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adjust the width of project avatars to fix alignment within their container -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml deleted file mode 100644 index cc8b0e28277..00000000000 --- a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sentence cased the nav tab headers on the project dashboard page -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml deleted file mode 100644 index 2c3ba1dfe44..00000000000 --- a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds hoverstates for collapsed Issue/Merge Request sidebar -merge_request: !7777 -author: diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml deleted file mode 100644 index a7b5810f1bf..00000000000 --- a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redirect to sign-in page when unauthenticated user tries to create a snippet -merge_request: 7786 -author: diff --git a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml deleted file mode 100644 index 862de7c5db1..00000000000 --- a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not raise error in AutocompleteController#users when not authorized -merge_request: 7817 -author: Semyon Pupkov diff --git a/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml b/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml deleted file mode 100644 index 18836e7a90b..00000000000 --- a/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Hides new issue button for non loggedin user -merge_request: 8175 -author: diff --git a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml deleted file mode 100644 index 1799fad1631..00000000000 --- a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjusted margins for Build Status and Coverage Report rows to match those of - the CI/CD Pipeline row -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml deleted file mode 100644 index 62030d3fc45..00000000000 --- a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent user creating issue or MR without signing in for a group -merge_request: 7902 -author: diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml deleted file mode 100644 index eab1534aa66..00000000000 --- a/changelogs/unreleased/25136-last-deployment-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Latest deployment link is broken -merge_request: 7839 -author: diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml deleted file mode 100644 index 531b0f83099..00000000000 --- a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve help message for issue create slash command -merge_request: 7850 -author: diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml deleted file mode 100644 index 7afc794866b..00000000000 --- a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Centered Accept Merge Request button within MR widget and added padding for - viewports smaller than 768px -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml deleted file mode 100644 index 69348281a50..00000000000 --- a/changelogs/unreleased/25207-text-overflow-env-table.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent enviroment table to overflow when name has underscores -merge_request: 8142 -author: diff --git a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml deleted file mode 100644 index 52de34478f0..00000000000 --- a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added top margin to Build status page header for mobile views -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml deleted file mode 100644 index 7f1c417bc77..00000000000 --- a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" - on MR pages' -merge_request: -author: diff --git a/changelogs/unreleased/25264-ref-commit.yml b/changelogs/unreleased/25264-ref-commit.yml deleted file mode 100644 index 13a33da9801..00000000000 --- a/changelogs/unreleased/25264-ref-commit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change ref property to commitRef in vue commit component -merge_request: 7901 -author: diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml deleted file mode 100644 index 79cb2c6d843..00000000000 --- a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix comments activity tab visibility condition' -merge_request: 7913 -author: Rydkin Maxim diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml deleted file mode 100644 index 567294fe5f7..00000000000 --- a/changelogs/unreleased/25294-remove-signed-out-msg.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'fix: removed signed_out notification' -merge_request: 7958 -author: jnoortheen diff --git a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml deleted file mode 100644 index afe57729c48..00000000000 --- a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Accept environment variables from the `pre-receive` script -merge_request: 7967 -author: diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml deleted file mode 100644 index 0770f9752a0..00000000000 --- a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed Housekeeping button on project settings page to default styling -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml deleted file mode 100644 index b12eab26b67..00000000000 --- a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure issuable state changes only fire webhooks once -merge_request: -author: diff --git a/changelogs/unreleased/25368-fix-left-align-system-note.yml b/changelogs/unreleased/25368-fix-left-align-system-note.yml deleted file mode 100644 index 81fd0888773..00000000000 --- a/changelogs/unreleased/25368-fix-left-align-system-note.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes left align issue for long system notes -merge_request: 7982 -author: diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml deleted file mode 100644 index 45a71b55b3b..00000000000 --- a/changelogs/unreleased/25374-svg-as-prop.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table" -merge_request: 7992 -author: diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml deleted file mode 100644 index 4c11fe1622e..00000000000 --- a/changelogs/unreleased/25482-fix-api-sudo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Memoize the current_user so that sudo can work properly' -merge_request: 8017 -author: diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml deleted file mode 100644 index d6c92014bea..00000000000 --- a/changelogs/unreleased/25483-broken-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab -merge_request: 8009 -author: diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml deleted file mode 100644 index c6a92547c5c..00000000000 --- a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added go back anchor on error pages. -merge_request: 8087 -author: diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml deleted file mode 100644 index 5d0adb04ef3..00000000000 --- a/changelogs/unreleased/25617-todos-filter-placeholder.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 25617 Fix placeholder color of todo filters -merge_request: -author: diff --git a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml deleted file mode 100644 index 7da8f9357a7..00000000000 --- a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Made the padding on the plus button in the breadcrumb menu even -merge_request: -author: Ryan Harris diff --git a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml deleted file mode 100644 index 0a81124de0d..00000000000 --- a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix colors and margins for adjacent alert banners -merge_request: 8151 -author: diff --git a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml deleted file mode 100644 index b9a8e17c64a..00000000000 --- a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure nil User-Agent doesn't break the CI API -merge_request: -author: diff --git a/changelogs/unreleased/25908-fix-grape-after-update.yml b/changelogs/unreleased/25908-fix-grape-after-update.yml deleted file mode 100644 index 026d5592441..00000000000 --- a/changelogs/unreleased/25908-fix-grape-after-update.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use Grape's new Route methods -merge_request: -author: diff --git a/changelogs/unreleased/25938-progress-bar-gone.yml b/changelogs/unreleased/25938-progress-bar-gone.yml deleted file mode 100644 index 841c4d445c6..00000000000 --- a/changelogs/unreleased/25938-progress-bar-gone.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds back CSS for progress-bars -merge_request: 8237 -author: diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml deleted file mode 100644 index 9de739d0cad..00000000000 --- a/changelogs/unreleased/4269-public-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow unauthenticated access to some Project API GET endpoints -merge_request: 7843 -author: diff --git a/changelogs/unreleased/4269-public-files-api.yml b/changelogs/unreleased/4269-public-files-api.yml deleted file mode 100644 index e8f9e9b5ed3..00000000000 --- a/changelogs/unreleased/4269-public-files-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow unauthenticated access to Repositories Files API GET endpoints -merge_request: -author: diff --git a/changelogs/unreleased/4269-public-repositories-api.yml b/changelogs/unreleased/4269-public-repositories-api.yml deleted file mode 100644 index 38984eed904..00000000000 --- a/changelogs/unreleased/4269-public-repositories-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow unauthenticated access to Repositories API GET endpoints -merge_request: 8148 -author: diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml deleted file mode 100644 index 9dd04d3f089..00000000000 --- a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Add setting to enable/disable HTML emails -merge_request: 7749 -author: diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml deleted file mode 100644 index a40dcde1393..00000000000 --- a/changelogs/unreleased/8003-katex-math.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added support for math rendering, using KaTeX, in Markdown and asciidoc -merge_request: 8003 -author: Munken diff --git a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml deleted file mode 100644 index 36f8ac9c840..00000000000 --- a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Authentiq as Oauth provider -merge_request: 8038 -author: Alexandros Keramidas diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml deleted file mode 100644 index 47478a2048b..00000000000 --- a/changelogs/unreleased/abuse_report-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for abuse_reports_spec -merge_request: 7644 -author: winniehell diff --git a/changelogs/unreleased/adam-auto-deploy.yml b/changelogs/unreleased/adam-auto-deploy.yml deleted file mode 100644 index 9d3348468d5..00000000000 --- a/changelogs/unreleased/adam-auto-deploy.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce "Set up autodeploy" button to help configure GitLab CI for deployment -merge_request: 8135 -author: diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml deleted file mode 100644 index a4b0354a9c9..00000000000 --- a/changelogs/unreleased/add_info_to_qr.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add GitLab host to 2FA QR code and manual info -merge_request: 6941 -author: diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml deleted file mode 100644 index 7989f94e528..00000000000 --- a/changelogs/unreleased/allow-more-filenames.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow all alphanumeric characters in file names -merge_request: 8002 -author: winniehell diff --git a/changelogs/unreleased/api-branch-status.yml b/changelogs/unreleased/api-branch-status.yml deleted file mode 100644 index c5763345a22..00000000000 --- a/changelogs/unreleased/api-branch-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Expose merge status for branch API' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml deleted file mode 100644 index 5f4cee450b9..00000000000 --- a/changelogs/unreleased/api-cherry-pick.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Ability to cherry pick a commit' -merge_request: 8047 -author: Robert Schilling diff --git a/changelogs/unreleased/api-delete-group-share.yml b/changelogs/unreleased/api-delete-group-share.yml deleted file mode 100644 index 26cfb35bba3..00000000000 --- a/changelogs/unreleased/api-delete-group-share.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Add ability to unshare a project from a group' -merge_request: 7662 -author: Robert Schilling diff --git a/changelogs/unreleased/api-expose-commiter-details.yml b/changelogs/unreleased/api-expose-commiter-details.yml deleted file mode 100644 index 5ee34adc5c9..00000000000 --- a/changelogs/unreleased/api-expose-commiter-details.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Expose committer details for commits' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml deleted file mode 100644 index d1b6507aedb..00000000000 --- a/changelogs/unreleased/api-remove-source-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Ability to set ''should_remove_source_branch'' on merge requests' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml deleted file mode 100644 index 54c8de610a6..00000000000 --- a/changelogs/unreleased/api-simple-group-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Simple representation of group''s projects' -merge_request: 8060 -author: Robert Schilling diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml deleted file mode 100644 index 1f9904c0691..00000000000 --- a/changelogs/unreleased/awards_handler.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for awards_handler_spec -merge_request: 7661 -author: winniehell diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml deleted file mode 100644 index 97d82518b7b..00000000000 --- a/changelogs/unreleased/bitbucket-oauth2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor Bitbucket importer to use BitBucket API Version 2 -merge_request: -author: diff --git a/changelogs/unreleased/cache-last-commit-sha-for-path.yml b/changelogs/unreleased/cache-last-commit-sha-for-path.yml deleted file mode 100644 index 9cd8c5bab86..00000000000 --- a/changelogs/unreleased/cache-last-commit-sha-for-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cache last commit id for path -merge_request: 8098 -author: Hiroyuki Sato diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml deleted file mode 100644 index b5dc3792745..00000000000 --- a/changelogs/unreleased/change_development_build_fixtures.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures -merge_request: -author: twonegatives diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml deleted file mode 100644 index f70607df7a1..00000000000 --- a/changelogs/unreleased/chomp-git-status-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: For single line git commit messages, the close quote should be on the same - line as the open quote -merge_request: -author: diff --git a/changelogs/unreleased/cleanup-common_utils-js.yml b/changelogs/unreleased/cleanup-common_utils-js.yml deleted file mode 100644 index 54d81b76c28..00000000000 --- a/changelogs/unreleased/cleanup-common_utils-js.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Clean up common_utils.js -merge_request: 7318 -author: winniehell diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml deleted file mode 100644 index 824c1c88a60..00000000000 --- a/changelogs/unreleased/comments-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for notes_spec -merge_request: 7683 -author: winniehell diff --git a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml deleted file mode 100644 index f0d9ff0c34f..00000000000 --- a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create dynamic fixture for build_spec -merge_request: 7589 -author: winniehell diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml deleted file mode 100644 index e713e2dc424..00000000000 --- a/changelogs/unreleased/destroy-session.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Destroy a user's session when they delete their own account -merge_request: -author: diff --git a/changelogs/unreleased/dev-issue-24554.yml b/changelogs/unreleased/dev-issue-24554.yml deleted file mode 100644 index 0bb362b9325..00000000000 --- a/changelogs/unreleased/dev-issue-24554.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Edit help text to clarify annotated tag creation. -merge_request: -author: Liz Lam diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml deleted file mode 100644 index 12b1460f388..00000000000 --- a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not reload diff for merge request made from fork when target branch in fork is updated -merge_request: 7973 -author: diff --git a/changelogs/unreleased/dockerfile-templates.yml b/changelogs/unreleased/dockerfile-templates.yml deleted file mode 100644 index e4db46cdf9a..00000000000 --- a/changelogs/unreleased/dockerfile-templates.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add support for Dockerfile templates -merge_request: 7247 -author: diff --git a/changelogs/unreleased/dz-allow-nested-group-routing.yml b/changelogs/unreleased/dz-allow-nested-group-routing.yml deleted file mode 100644 index 9d8e6e17914..00000000000 --- a/changelogs/unreleased/dz-allow-nested-group-routing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add nested groups support to the routing -merge_request: 7459 -author: diff --git a/changelogs/unreleased/dz-fix-route-rename.yml b/changelogs/unreleased/dz-fix-route-rename.yml deleted file mode 100644 index a649fb169a5..00000000000 --- a/changelogs/unreleased/dz-fix-route-rename.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Route#rename_children behavior -merge_request: -author: diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml deleted file mode 100644 index c227c5a8ea5..00000000000 --- a/changelogs/unreleased/dz-nested-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add nested groups support on data level -merge_request: -author: diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml deleted file mode 100644 index 90af42da01c..00000000000 --- a/changelogs/unreleased/dz-rename-invalid-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename groups with .git in the end of the path -merge_request: 8199 -author: diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml deleted file mode 100644 index 2787a5c57df..00000000000 --- a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow projects with 'dashboard' as path -merge_request: -author: diff --git a/changelogs/unreleased/dz-whitelist-more-project-names-2.yml b/changelogs/unreleased/dz-whitelist-more-project-names-2.yml deleted file mode 100644 index 5d5f57d79e9..00000000000 --- a/changelogs/unreleased/dz-whitelist-more-project-names-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Whitelist next project names: notes, services' -merge_request: -author: diff --git a/changelogs/unreleased/dz-whitelist-more-project-names.yml b/changelogs/unreleased/dz-whitelist-more-project-names.yml deleted file mode 100644 index 4a3f1511a0b..00000000000 --- a/changelogs/unreleased/dz-whitelist-more-project-names.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Whitelist next project names: help, ci, admin, search' -merge_request: 8227 -author: diff --git a/changelogs/unreleased/emoji-btn-disabled.yml b/changelogs/unreleased/emoji-btn-disabled.yml deleted file mode 100644 index a18b553d513..00000000000 --- a/changelogs/unreleased/emoji-btn-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disabled emoji buttons when user is not logged in -merge_request: -author: diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml deleted file mode 100644 index 9c52e53c3b4..00000000000 --- a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable AsciiDoctor admonition icons -merge_request: 7812 -author: Horacio Sanson diff --git a/changelogs/unreleased/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml deleted file mode 100644 index 7663d5b6ae5..00000000000 --- a/changelogs/unreleased/expose-deployment-variables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass variables from deployment project services to CI runner -merge_request: 8107 -author: diff --git a/changelogs/unreleased/feature-admin-user-groups-link.yml b/changelogs/unreleased/feature-admin-user-groups-link.yml deleted file mode 100644 index b89c08f82d7..00000000000 --- a/changelogs/unreleased/feature-admin-user-groups-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: The admin user projects view now has a clickable group link -merge_request: 7620 -author: James Gregory diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml deleted file mode 100644 index 80c7bb75359..00000000000 --- a/changelogs/unreleased/features-api-snippets.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Endpoint to expose personal snippets as /snippets' -merge_request: 6373 -author: Bernard Guyzmo Pratz diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml deleted file mode 100644 index cf2a622b7e6..00000000000 --- a/changelogs/unreleased/file-template-dropwdown-proper-position.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens -merge_request: -author: diff --git a/changelogs/unreleased/fix-cancelling-pipelines.yml b/changelogs/unreleased/fix-cancelling-pipelines.yml deleted file mode 100644 index c21e663093a..00000000000 --- a/changelogs/unreleased/fix-cancelling-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix cancelling created or external pipelines -merge_request: 7508 -author: diff --git a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml deleted file mode 100644 index e37841e80c3..00000000000 --- a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create builds in transaction to avoid empty pipelines -merge_request: 7742 -author: diff --git a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml deleted file mode 100644 index 0d11969575a..00000000000 --- a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use authorized projects in ProjectTeam -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-export-build-token.yml b/changelogs/unreleased/fix-import-export-build-token.yml deleted file mode 100644 index 622487e6829..00000000000 --- a/changelogs/unreleased/fix-import-export-build-token.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Import/Export duplicated builds error -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-export-ee-services.yml b/changelogs/unreleased/fix-import-export-ee-services.yml deleted file mode 100644 index c0aacbc96f8..00000000000 --- a/changelogs/unreleased/fix-import-export-ee-services.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix missing service error importing from EE to CE -merge_request: 8144 -author: diff --git a/changelogs/unreleased/fix-import-export-mr-error.yml b/changelogs/unreleased/fix-import-export-mr-error.yml deleted file mode 100644 index e1137bca131..00000000000 --- a/changelogs/unreleased/fix-import-export-mr-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Import/Export merge requests error while importing -merge_request: -author: diff --git a/changelogs/unreleased/fix-import-labels-error.yml b/changelogs/unreleased/fix-import-labels-error.yml deleted file mode 100644 index 86cae3a49ff..00000000000 --- a/changelogs/unreleased/fix-import-labels-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix project import label priorities error -merge_request: -author: diff --git a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml deleted file mode 100644 index f8acc6ef8ad..00000000000 --- a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename Merge When Build Succeeds to Merge When Pipeline Succeeds -merge_request: 7135 -author: diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml deleted file mode 100644 index aa5ad5cd8d6..00000000000 --- a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Slack pipeline message from pipelines made by API -merge_request: 8059 -author: diff --git a/changelogs/unreleased/fix-yaml-variables.yml b/changelogs/unreleased/fix-yaml-variables.yml deleted file mode 100644 index 3abff1e3b08..00000000000 --- a/changelogs/unreleased/fix-yaml-variables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Convert CI YAML variables keys into strings -merge_request: 8088 -author: diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml deleted file mode 100644 index 46b6702d9fd..00000000000 --- a/changelogs/unreleased/gem-update-grape.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Gem update: Update grape to 0.18.0' -merge_request: -author: Robert Schilling diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml deleted file mode 100644 index 23c2139cf93..00000000000 --- a/changelogs/unreleased/gitlab-workhorse-multipart.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace Rack::Multipart with GitLab-Workhorse based solution -merge_request: 5867 -author: diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml deleted file mode 100644 index 6d60f23c798..00000000000 --- a/changelogs/unreleased/glm-shorthand-reference.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add shorthand support to gitlab markdown references -merge_request: 7255 -author: Oswaldo Ferreira diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml deleted file mode 100644 index 73d8a52e001..00000000000 --- a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add git diff context to notifications of new notes on merge requests -merge_request: -author: Heidi Hoopes diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml deleted file mode 100644 index 8f8bbc51963..00000000000 --- a/changelogs/unreleased/html-safe-diff-line-content.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't accidentally mark unsafe diff lines as HTML safe -merge_request: -author: diff --git a/changelogs/unreleased/improve-invite-accept-page.yml b/changelogs/unreleased/improve-invite-accept-page.yml deleted file mode 100644 index 8a09a5ae42f..00000000000 --- a/changelogs/unreleased/improve-invite-accept-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add note to the invite page when the logged in user email is not the same as the invitation -merge_request: -author: diff --git a/changelogs/unreleased/issuable_filters_present-refactor.yml b/changelogs/unreleased/issuable_filters_present-refactor.yml deleted file mode 100644 index c131f9cb68e..00000000000 --- a/changelogs/unreleased/issuable_filters_present-refactor.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor issuable_filters_present to reduce duplications -merge_request: 7776 -author: Semyon Pupkov diff --git a/changelogs/unreleased/issue-24534.yml b/changelogs/unreleased/issue-24534.yml deleted file mode 100644 index 14d6730d3f6..00000000000 --- a/changelogs/unreleased/issue-24534.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary sentences for status codes in the API documentation -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue-boards-scrollable-element.yml b/changelogs/unreleased/issue-boards-scrollable-element.yml deleted file mode 100644 index 90edc30e791..00000000000 --- a/changelogs/unreleased/issue-boards-scrollable-element.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards scrolling with a lot of lists & issues -merge_request: -author: diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml deleted file mode 100644 index a3b08bde6e7..00000000000 --- a/changelogs/unreleased/issue-events-filter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add issue events filter and make all really show all events -merge_request: 7673 -author: Oxan van Leeuwen diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml deleted file mode 100644 index 9c15c436876..00000000000 --- a/changelogs/unreleased/issue_13270.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to delete tag release note -merge_request: -author: diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml deleted file mode 100644 index 6b7164aff77..00000000000 --- a/changelogs/unreleased/issue_22269.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create mattermost service -merge_request: -author: diff --git a/changelogs/unreleased/issue_24363.yml b/changelogs/unreleased/issue_24363.yml deleted file mode 100644 index 0298890b477..00000000000 --- a/changelogs/unreleased/issue_24363.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix appearance in error pages -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue_24748.yml b/changelogs/unreleased/issue_24748.yml deleted file mode 100644 index 4c1df542c53..00000000000 --- a/changelogs/unreleased/issue_24748.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix title case to sentence case -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issue_24958.yml b/changelogs/unreleased/issue_24958.yml deleted file mode 100644 index dbbbbf9d28d..00000000000 --- a/changelogs/unreleased/issue_24958.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix bad selection on dropdown menu for tags filter -merge_request: -author: Luis Alonso Chavez Armendariz diff --git a/changelogs/unreleased/issues-1608-text.yml b/changelogs/unreleased/issues-1608-text.yml deleted file mode 100644 index bef427a1e1e..00000000000 --- a/changelogs/unreleased/issues-1608-text.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: change text around timestamps to make it clear which timestamp is displayed -merge_request: 7860 -author: BM5k diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml deleted file mode 100644 index 9d2edcfee42..00000000000 --- a/changelogs/unreleased/jej-22869.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix information disclosure in `Projects::BlobController#update` -merge_request: -author: diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml deleted file mode 100644 index 5a4a44b9562..00000000000 --- a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace MR access checks with use of MergeRequestsFinder -merge_request: -author: diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml deleted file mode 100644 index db1389e2024..00000000000 --- a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use -merge_request: -author: diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml deleted file mode 100644 index 844fba9a107..00000000000 --- a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix missing access checks on issue lookup using IssuableFinder -merge_request: -author: diff --git a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml deleted file mode 100644 index ad6eba3faf2..00000000000 --- a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix N+1 queries on milestone show pages -merge_request: 8185 -author: diff --git a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml deleted file mode 100644 index ab7f39a4178..00000000000 --- a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Milestoneish SQL performance partially improved and memoized -merge_request: 8146 -author: diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml deleted file mode 100644 index 1768bdfd487..00000000000 --- a/changelogs/unreleased/jej-note-search-uses-finder.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix missing Note access checks by moving Note#search to updated NoteFinder -merge_request: -author: diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml deleted file mode 100644 index c0b6f50052c..00000000000 --- a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace issue access checks with use of IssuableFinder -merge_request: -author: diff --git a/changelogs/unreleased/leave-project-btn.yml b/changelogs/unreleased/leave-project-btn.yml deleted file mode 100644 index 2aa553d7b97..00000000000 --- a/changelogs/unreleased/leave-project-btn.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move all action buttons to project header -merge_request: -author: diff --git a/changelogs/unreleased/mattermost-slash-auto-config.yml b/changelogs/unreleased/mattermost-slash-auto-config.yml deleted file mode 100644 index 43014d38769..00000000000 --- a/changelogs/unreleased/mattermost-slash-auto-config.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to auto-configure Mattermost -merge_request: 8070 -author: diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml deleted file mode 100644 index b15403d6d62..00000000000 --- a/changelogs/unreleased/members-dropdowns.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updated members dropdowns -merge_request: -author: diff --git a/changelogs/unreleased/milestone_start_date.yml b/changelogs/unreleased/milestone_start_date.yml deleted file mode 100644 index 39ac1344329..00000000000 --- a/changelogs/unreleased/milestone_start_date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a starting date to milestones -merge_request: -author: diff --git a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml deleted file mode 100644 index 9de7477c200..00000000000 --- a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move abuse report spinach test to rspec -merge_request: 7659 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml deleted file mode 100644 index fb70fa2955a..00000000000 --- a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin abuse report spinach test to rspec -merge_request: 7691 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml deleted file mode 100644 index 11250643a23..00000000000 --- a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin active tab spinach tests to rspec -merge_request: 8037 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml deleted file mode 100644 index 7dfd741985a..00000000000 --- a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin hooks spinach to rspec -merge_request: 7942 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml deleted file mode 100644 index 696aa8510a0..00000000000 --- a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin logs spinach test to rspec -merge_request: 7945 -author: Semyon Pupkov diff --git a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml deleted file mode 100644 index a7ec2c20554..00000000000 --- a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move admin spam spinach test to Rspec -merge_request: 7708 -author: Semyon Pupkov diff --git a/changelogs/unreleased/mr-origin-7855.yml b/changelogs/unreleased/mr-origin-7855.yml deleted file mode 100644 index 0fdc6153d55..00000000000 --- a/changelogs/unreleased/mr-origin-7855.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Provides a sensible default message when adding a README to a project -merge_request: 7903 -author: diff --git a/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml b/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml deleted file mode 100644 index bb4edf80d94..00000000000 --- a/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add online terminal support for Kubernetes -merge_request: 7690 -author: diff --git a/changelogs/unreleased/pipeline-build-hitbox.yml b/changelogs/unreleased/pipeline-build-hitbox.yml deleted file mode 100644 index 051b538a9a3..00000000000 --- a/changelogs/unreleased/pipeline-build-hitbox.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make CI badge hitboxes match parent -merge_request: -author: diff --git a/changelogs/unreleased/post_receive-any-email.yml b/changelogs/unreleased/post_receive-any-email.yml deleted file mode 100644 index 3710b1b4b46..00000000000 --- a/changelogs/unreleased/post_receive-any-email.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "post_receive: accept any user email from last commit" -merge_request: 7225 -author: Elan Ruusamäe diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml deleted file mode 100644 index 26aabd9b647..00000000000 --- a/changelogs/unreleased/process-commit-worker-migration-encoding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors -merge_request: -author: diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml deleted file mode 100644 index f5e844470b2..00000000000 --- a/changelogs/unreleased/public-tags-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow public access to some Tag API endpoints -merge_request: -author: diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml deleted file mode 100644 index 211d3b80c3a..00000000000 --- a/changelogs/unreleased/readme-link-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken README.md UX guide link. -merge_request: -author: diff --git a/changelogs/unreleased/refactor-create-service-spec.yml b/changelogs/unreleased/refactor-create-service-spec.yml deleted file mode 100644 index 148a0fee02c..00000000000 --- a/changelogs/unreleased/refactor-create-service-spec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor create service spec -merge_request: 7609 -author: Semyon Pupkov diff --git a/changelogs/unreleased/remove-backup-strategies.yml b/changelogs/unreleased/remove-backup-strategies.yml deleted file mode 100644 index 9f034613c2c..00000000000 --- a/changelogs/unreleased/remove-backup-strategies.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Stop supporting Google and Azure as backup strategies -merge_request: -author: diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml deleted file mode 100644 index 6e02998b3a8..00000000000 --- a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc - plugins and envs -merge_request: 7470 -author: Luke "Jared" Bennett diff --git a/changelogs/unreleased/remove-require-from-services.yml b/changelogs/unreleased/remove-require-from-services.yml deleted file mode 100644 index 400512e0314..00000000000 --- a/changelogs/unreleased/remove-require-from-services.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Remove unnecessary require_relative calls from service classes' -merge_request: '7601' -author: Semyon Pupkov diff --git a/changelogs/unreleased/remove-u2f-error-logging.yml b/changelogs/unreleased/remove-u2f-error-logging.yml deleted file mode 100644 index edbe576a976..00000000000 --- a/changelogs/unreleased/remove-u2f-error-logging.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display error code for U2F errors -merge_request: 7305 -author: winniehell diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml deleted file mode 100644 index 754af641add..00000000000 --- a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary commits order message -merge_request: 8004 -author: diff --git a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml deleted file mode 100644 index bef11c63675..00000000000 --- a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary self from user model -merge_request: 7551 -author: Semyon Pupkov diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml deleted file mode 100644 index 01314ab5585..00000000000 --- a/changelogs/unreleased/removing_unnecessary_indexes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary database indices -merge_request: -author: diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml deleted file mode 100644 index 827b0dbb1d3..00000000000 --- a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Render SVG images in diffs and notes -merge_request: 7747 -author: andrebsguedes diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml deleted file mode 100644 index 46a3e459fef..00000000000 --- a/changelogs/unreleased/right-sidebar-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for right_sidebar_spec -merge_request: 7687 -author: winniehell diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml deleted file mode 100644 index e0fbc6e3b5a..00000000000 --- a/changelogs/unreleased/rounded-labels-fixes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Additional rounded label fixes -merge_request: -author: diff --git a/changelogs/unreleased/rs-project-team-helpers.yml b/changelogs/unreleased/rs-project-team-helpers.yml deleted file mode 100644 index 79abcbce1e3..00000000000 --- a/changelogs/unreleased/rs-project-team-helpers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add shortcuts for adding users to a project team with a specific role -merge_request: -author: Nikolay Ponomarev and Dino M diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml deleted file mode 100644 index e8153be043a..00000000000 --- a/changelogs/unreleased/seed-runner-token.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for setting the GitLab Runners Registration Token during initial - database seeding -merge_request: 6642 -author: diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml deleted file mode 100644 index 88945600886..00000000000 --- a/changelogs/unreleased/shortcuts-issuable-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for shortcuts_issuable_spec -merge_request: 7685 -author: winniehell diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml deleted file mode 100644 index bbd7a217493..00000000000 --- a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show commit status from latest pipeline -merge_request: 7333 -author: diff --git a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml deleted file mode 100644 index ca11e3b94a7..00000000000 --- a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Simplify copy on "Create a new list" dropdown in Issue Boards -merge_request: 7605 -author: Victor Rodrigues diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml deleted file mode 100644 index 804bd05b613..00000000000 --- a/changelogs/unreleased/small-emoji-adjustments.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Various small emoji positioning adjustments -merge_request: -author: diff --git a/changelogs/unreleased/update-api-spec-files.yml b/changelogs/unreleased/update-api-spec-files.yml deleted file mode 100644 index 349d866cf22..00000000000 --- a/changelogs/unreleased/update-api-spec-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update API spec files to describe the correct class -merge_request: -author: Livier diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml deleted file mode 100644 index ddb3c1c8da4..00000000000 --- a/changelogs/unreleased/update-button-font-weight.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updates the font weight of button styles because of the change to system fonts -merge_request: -author: diff --git a/changelogs/unreleased/update-git-version-in-doc.yml b/changelogs/unreleased/update-git-version-in-doc.yml deleted file mode 100644 index cb3260f71cd..00000000000 --- a/changelogs/unreleased/update-git-version-in-doc.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump Git version requirement to 2.8.4 -merge_request: -author: diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml deleted file mode 100644 index e4395461560..00000000000 --- a/changelogs/unreleased/use-st-commits-where-possible.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace references to MergeRequestDiff#commits with st_commits when we care - only about the number of commits -merge_request: 7668 -author: diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml deleted file mode 100644 index 1ffb3eb435c..00000000000 --- a/changelogs/unreleased/username-exists-root.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Username exists check respects relative root path -merge_request: -author: diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml deleted file mode 100644 index 3fb025806b0..00000000000 --- a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Validate state param when filtering issuables -merge_request: -author: diff --git a/changelogs/unreleased/zen-mode-fixture.yml b/changelogs/unreleased/zen-mode-fixture.yml deleted file mode 100644 index bec6f6e6dba..00000000000 --- a/changelogs/unreleased/zen-mode-fixture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace static fixture for zen_mode_spec -merge_request: 7686 -author: winniehell diff --git a/changelogs/unreleased/zj-expose-coverage-pipelines.yml b/changelogs/unreleased/zj-expose-coverage-pipelines.yml deleted file mode 100644 index 34e4926e58a..00000000000 --- a/changelogs/unreleased/zj-expose-coverage-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: expose pipeline coverage' -merge_request: -author: diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml deleted file mode 100644 index ae4824f82fa..00000000000 --- a/changelogs/unreleased/zj-fix-label-creation-non-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Non members cannot create labels through the API -merge_request: -author: diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml deleted file mode 100644 index 1859addd606..00000000000 --- a/changelogs/unreleased/zj-guest-reads-public-builds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Guests can read builds when public -merge_request: 6842 -author: diff --git a/changelogs/unreleased/zj-issue-new-over-issue-create.yml b/changelogs/unreleased/zj-issue-new-over-issue-create.yml deleted file mode 100644 index 9dd463e4efa..00000000000 --- a/changelogs/unreleased/zj-issue-new-over-issue-create.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Accept issue new as command to create an issue -merge_request: -author: diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml deleted file mode 100644 index de41c39d545..00000000000 --- a/changelogs/unreleased/zj-issue-search-slash-command.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add issue search slash command -merge_request: -author: diff --git a/changelogs/unreleased/zj-remove-unused-services.yml b/changelogs/unreleased/zj-remove-unused-services.yml deleted file mode 100644 index 8ede95f5faa..00000000000 --- a/changelogs/unreleased/zj-remove-unused-services.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unused and void services from the database -merge_request: -author: diff --git a/changelogs/unreleased/zj-slack-slash-commands.yml b/changelogs/unreleased/zj-slack-slash-commands.yml deleted file mode 100644 index 9f4c8681ad0..00000000000 --- a/changelogs/unreleased/zj-slack-slash-commands.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor presenters ChatCommands -merge_request: 7846 -author: diff --git a/changelogs/unreleased/zj-use-ruby-2-3-3.yml b/changelogs/unreleased/zj-use-ruby-2-3-3.yml deleted file mode 100644 index 0d1a0fcd79d..00000000000 --- a/changelogs/unreleased/zj-use-ruby-2-3-3.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump ruby version to 2.3.3 -merge_request: 7904 -author: From 47550d092f0a6cbedc58752d1a220fe519b8ea01 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 22 Dec 2016 14:51:57 -0200 Subject: [PATCH 179/206] Update VERSION to 8.16.0-pre --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d59bc5cbc5c..8e9258150a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.15.0-pre +8.16.0-pre From 4213e8ac691d3c98a44b693951eb5439b0859772 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 22 Dec 2016 18:02:13 +0100 Subject: [PATCH 180/206] Slack docs [ci skip] --- doc/project_services/img/slack_setup.png | Bin 0 -> 126412 bytes doc/project_services/slack_slash_commands.md | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 doc/project_services/img/slack_setup.png create mode 100644 doc/project_services/slack_slash_commands.md diff --git a/doc/project_services/img/slack_setup.png b/doc/project_services/img/slack_setup.png new file mode 100644 index 0000000000000000000000000000000000000000..f69817f2b78d6621915fcb1b0cc61359e7fe812d GIT binary patch literal 126412 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 ev~M>2y3CN&m6*U Introduced in GitLab 8.15 + +Slack commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in chat. +For all available commands try the help subcommand, for example: `/gitlab help` + +## Prerequisites + +A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be create beforehand, GitLab can not create it for you. + +## Configuration + +First, navigate to the Slack Slash commands service page, found at your project's +**Settings** > **Services**, and you find the instructions there: + + ![Slack setup instructions](img/slack_setup.png) + +Once you've followed the instructions, mark the service as active and insert the token +you've received from Slack. After saving the service you are good to go! From 3cace951cd2dc8fbe3aa33cf984e001a1104d388 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Thu, 22 Dec 2016 19:08:18 +0100 Subject: [PATCH 181/206] Update architecture diagram --- .../gitlab_architecture_diagram.png | Bin 61944 -> 61667 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png index ec1eaab179a0a6cf8c4c5694c160b603b1ff7d8e..378f73845744220d25af57dbcf21382ff658b55a 100644 GIT binary patch delta 12488 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

C<> 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 delta 12685 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 Date: Thu, 22 Dec 2016 18:58:39 +0100 Subject: [PATCH 182/206] List all commands [ci skip] --- doc/integration/chat_commands.md | 14 ++++++++++++++ doc/project_services/slack_slash_commands.md | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 doc/integration/chat_commands.md diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md new file mode 100644 index 00000000000..4b0084678d9 --- /dev/null +++ b/doc/integration/chat_commands.md @@ -0,0 +1,14 @@ +# Chat Commands + +Chat commands allow user to perform common operations on GitLab right from there chat client. +Right now both Mattermost and Slack are supported. + +## Available commands + +The trigger is configurable, but for the sake of this example, we'll use `/trigger` + +* `/trigger help` - Displays all available commands for this user +* `/trigger issue new <shift+return> <description>` - creates a new issue on the project +* `/trigger issue show <id>` - Shows the issue with the given ID, if you've got access +* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query +* `/trigger deploy <from> to <to>` - Deploy from an environment to another diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md index 92d4e2afd29..b6b5c741d90 100644 --- a/doc/project_services/slack_slash_commands.md +++ b/doc/project_services/slack_slash_commands.md @@ -5,11 +5,12 @@ Slack commands give users an extra interface to perform common operations from the chat environment. This allows one to, for example, create an issue as soon as the idea was discussed in chat. -For all available commands try the help subcommand, for example: `/gitlab help` +For all available commands try the help subcommand, for example: `/gitlab help`, +all review the [full list of commands](../integrations/chat_commands.md). ## Prerequisites -A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be create beforehand, GitLab can not create it for you. +A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. ## Configuration From 6dc3efdd9896156b5b08c735f1639437ff874fc0 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Thu, 22 Dec 2016 16:12:35 -0200 Subject: [PATCH 183/206] Do not override incoming webhook channel for mattermost and slack --- .../project_services/chat_notification_service.rb | 10 ++++++---- app/models/project_services/mattermost_service.rb | 2 +- app/models/project_services/slack_service.rb | 2 +- changelogs/unreleased/issue_25887.yml | 4 ++++ .../slack_mattermost_notifications_shared_examples.rb | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/issue_25887.yml diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index b0556987721..475344d80ce 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -49,10 +49,12 @@ class ChatNotificationService < Service return false unless message - opt = {} + channel_name = get_channel_field(object_kind).presence || channel - opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel + opt = {} + opt[:channel] = channel_name if channel_name opt[:username] = username if username + notifier = Slack::Notifier.new(webhook, opt) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) @@ -71,7 +73,7 @@ class ChatNotificationService < Service fields.reject { |field| field[:name].end_with?('channel') } end - def default_channel + def default_channel_placeholder raise NotImplementedError end @@ -103,7 +105,7 @@ class ChatNotificationService < Service def build_event_channels supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder } end end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 0650f930402..ee8a0b55275 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -35,7 +35,7 @@ class MattermostService < ChatNotificationService ] end - def default_channel + def default_channel_placeholder "#town-square" end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 0583470d3b5..76d233a3cca 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -34,7 +34,7 @@ class SlackService < ChatNotificationService ] end - def default_channel + def default_channel_placeholder "#general" end end diff --git a/changelogs/unreleased/issue_25887.yml b/changelogs/unreleased/issue_25887.yml new file mode 100644 index 00000000000..27299bbc5f9 --- /dev/null +++ b/changelogs/unreleased/issue_25887.yml @@ -0,0 +1,4 @@ +--- +title: Do not override incoming webhook for mattermost and slack +merge_request: +author: diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 8582aea5fe5..74d9b8c6313 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -105,7 +105,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do allow(chat_service).to receive(:username).and_return(username) expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username, channel: chat_service.default_channel). + with(webhook_url, username: username). and_return( double(:slack_service).as_null_object ) From 940d8a06422b94ee7a5ff36d91a820fff3d9947b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 22 Dec 2016 18:39:00 +0000 Subject: [PATCH 184/206] Added tests for special characters --- spec/features/issues/gfm_autocomplete_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index df3a467cbb7..d0294908d2c 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -47,6 +47,24 @@ feature 'GFM autocomplete', feature: true, js: true do expect_to_wrap(true, label_item, note, label.title) end + it "does not show drpdown when preceded with a special character" do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys("@") + note.click + end + + expect(page).to have_selector('.atwho-container') + + page.within '.timeline-content-form' do + note.native.send_keys("@") + note.click + end + + expect(page).to have_selector('.atwho-container', visible: false) + end + it 'doesn\'t wrap for assignee values' do note = find('#note_note') page.within '.timeline-content-form' do From 7d7ba0b441bf600f3c047afdb1f7355ae690cd8a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 22 Dec 2016 18:38:44 +0000 Subject: [PATCH 185/206] Fix text overflow Fix text overflow in comments in MR Adds MR ID to CHANGELOG entry --- app/assets/stylesheets/pages/notes.scss | 29 ++++++++++++------- app/views/projects/notes/_note.html.haml | 2 +- ...discussion-actions-overlap-header-text.yml | 4 +++ 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/25930-discussion-actions-overlap-header-text.yml diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 6ac4ec6ea0d..b512da0939f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,7 +43,7 @@ ul.notes { } .system-note-message { - display: inline; + display: inline-block; &::first-letter { text-transform: lowercase; @@ -55,7 +55,7 @@ ul.notes { } p { - display: inline; + display: inline-block; margin: 0; &::first-letter { @@ -151,10 +151,6 @@ ul.notes { } } } - - .note-headline-light { - display: inline; - } } .discussion-body { @@ -452,11 +448,6 @@ ul.notes { border-radius: $border-radius-base; } -.diff-file .note .note-actions { - right: 0; - top: 0; -} - /** * Line note button on the side of diffs @@ -590,3 +581,19 @@ ul.notes { } } } + +// Merge request notes in diffs +.diff-file { + + // Diff is side by side + .notes_content.parallel .note-header .note-headline-light { + display: block; + position: relative; + } + + // Diff is inline + .notes_content .note-header .note-headline-light { + display: inline-block; + position: relative; + } +} diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 778a32e6345..399cf85cd0f 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -10,7 +10,7 @@ .timeline-content .note-header = link_to_member(note.project, note.author, avatar: false) - .inline.note-headline-light + .note-headline-light = note.author.to_reference - unless note.system commented diff --git a/changelogs/unreleased/25930-discussion-actions-overlap-header-text.yml b/changelogs/unreleased/25930-discussion-actions-overlap-header-text.yml new file mode 100644 index 00000000000..54a461c24ed --- /dev/null +++ b/changelogs/unreleased/25930-discussion-actions-overlap-header-text.yml @@ -0,0 +1,4 @@ +--- +title: Fix discussion overlap text in regular screens +merge_request: 8273 +author: From c60585a185d9e8ed0c4148a0a319e8672414c36f Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Thu, 22 Dec 2016 20:54:54 +0100 Subject: [PATCH 186/206] Change earlier to task_status_short to avoid titlebar line wraps --- app/helpers/issuables_helper.rb | 4 ++-- .../25941-odd-overflow-behavior-for-long-issue-headers.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8231f8fa334..1c213983a5b 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -96,8 +96,8 @@ module IssuablesHelper if issuable.tasks? output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg") + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") end output diff --git a/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml b/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml new file mode 100644 index 00000000000..c28cf7a0f86 --- /dev/null +++ b/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml @@ -0,0 +1,4 @@ +--- +title: Change earlier to task_status_short to avoid titlebar line wraps +merge_request: +author: From bed476041fab267b42e89d31d09a683d1b67b650 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 22 Dec 2016 23:54:08 +0200 Subject: [PATCH 187/206] Add images to issue creation from unresolved discussions [ci skip] --- .../img/resolve_discussion_issue_notice.png | Bin 0 -> 11123 bytes .../img/resolve_discussion_open_issue.png | Bin 0 -> 20967 bytes .../merge_request_discussion_resolution.md | 8 ++++++-- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png create mode 100644 doc/user/project/merge_requests/img/resolve_discussion_open_issue.png diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png new file mode 100644 index 0000000000000000000000000000000000000000..8c7ce215ae0a93029be9784eef842ae7f49e479b GIT binary patch literal 11123 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 Jss;1{{y*(?3_kz> literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png b/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png new file mode 100644 index 0000000000000000000000000000000000000000..98d632783268778572d3238cefa4af504a1c978c GIT binary patch literal 20967 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%0D%3<^)PVn1oCeWYum1c_Ps^yC z3nj~*I9g$oCnLNwO2U`T-Wst2`0-zZB5HqOfws}L;Fv|8SxNN@y7iF0=^uk}LfZ;s zu~I3Pg9h2xm7AB+G3tE_gfnH%meXn5UQ4xygXHru2Xwp02n`ypVn+JI8yy0?z?-ts zlA{C5^pTAM>zUoY*%epWO!o}hwD6DNo44U#?P|xkC1nDEe*ncQ=q0qZ-wKKH9=R;O zhZ~p9@fQvm#V)=KJKXH=qa^L$XH-O&e9r8ZiOFE{V9GI+#s2a!p?;{f42LlRI0#E3 z^pn*K%+=%=LxQv!l+8F`&RGy2U1n(u!)Jw1zB>HGS-Z5kj!MG4QgRtVN!=QLFuyXG z(yuLEup}z+o=nD!9=t~ZiG+}K$JxWSBWYyoN><M8Nc<l4&M?&=0M-85gKE8bnki3+ zG!}N{+8#vXa<V{)SDR`{<MiT`dGq-`w>T8Ok!4J6Vw07a7Gi+u7tUrfGqibUiw0`l z>DX(4$d8Cpyq1>ePq2WzrvrrS>ZJypdMth^A=T6^(31oFgJ^aMjiZ$<w@02BF4oqh z{4!v1-o_(|K25tLQO8E#k-L^6Pss&uMp~?mr-PJd1|WuA5<15{zlUCk5qT3CY{AL! z{X}$ImTivN@k0KfwwhTi8*4Xyuqu|y68aw&60r)@G-RG0nC2T7YJ2alC3+60rMETk zN=q}7`Lq*qz7E!sL<R_%dJ9*`F^5$-wDco2y+)14_<l>gs{?7EgDj9cO_qfzTIdh{ zozU@1(A0`Q&`#|tUx6eONi-+1I4-?vDV4Np|3CYGcqd>MJQ2b~<L~be3ZtAzB%LFv z0c1Ifp9AVO|KRu`X~E{qOm4r^1ML3FyOQW!my4vrw*trl6fQMmGkw3TuNUpCKgH=? z*~|+5JLmYn+_Q^U0ma{k&x;5oxBsp{Vtxsc6jq4*uGq#+Dsro3N+PSvy--GgR58PG z#Kpo1vh+M@J&1|wM7eO;4@Qu-h5%~0;4RO))=K(R;;Kw!L9sj4?H|Oi6Ut-xtCNUT T_VHK8XNZToGF+)x!7AwAz)OVF literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md index f37f1ce4d21..d4b85676d19 100644 --- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md +++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md @@ -51,13 +51,15 @@ are resolved. ![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png) -### Move all unresolved discussions in a merge request to an issue +## Move all unresolved discussions in a merge request to an issue -> [Introduced][ce-7180] (Currently on Backlog) +> [Introduced][ce-7180] in GitLab 8.15. To delegate unresolved discussions to a new issue you can click the link **open an issue to resolve them later**. +![Open new issue from unresolved discussions](img/resolve_discussion_open_issue.png) + This will prepare an issue with content referring to the merge request and discussions. @@ -66,6 +68,8 @@ discussions. Hitting **Submit issue** will cause all discussions to be marked as resolved and add a note referring to the newly created issue. +![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png) + You can now proceed to merge the merge request from the UI. [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 From 63d4db847eec79ee964ec58fb906e558d2019ff9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 22 Dec 2016 23:25:05 +0000 Subject: [PATCH 188/206] Fix hover not working on firefox --- app/assets/stylesheets/pages/pipelines.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 93c8b6c9a1e..8056e0701ef 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -863,7 +863,8 @@ } .builds-dropdown { - &:focus { + &:focus, + &:hover { outline: none; margin-right: -8px; From 19e6f22a0aa7834876dd1ebfa59e1a873e4e7e96 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 22 Dec 2016 23:31:52 +0000 Subject: [PATCH 189/206] Fix caret position in Safari --- app/assets/stylesheets/pages/pipelines.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8056e0701ef..00ddfc16b29 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -923,9 +923,9 @@ .dropdown-caret { font-size: 11px; - position: relative; - top: 3px; - left: -14px; + position: absolute;; + top: 5px; + left: 20px; margin-right: -6px; display: none; z-index: 2; From 716549e96b6537103c2cdeb124330325f96ff458 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 23 Dec 2016 00:18:57 +0000 Subject: [PATCH 190/206] Fix animation glitch in the caret --- app/assets/stylesheets/pages/pipelines.scss | 134 +++++++++----------- 1 file changed, 63 insertions(+), 71 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 00ddfc16b29..d75a5f42c2f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -789,6 +789,68 @@ color: $gl-text-color-light; border: none; margin: 0; + + &:focus, + &:hover { + outline: none; + margin-right: -8px; + + .ci-status-icon { + width: 32px; + padding: 0 8px 0 0; + transition: width 0.1s cubic-bezier(0.25, 0, 1, 1); + + + .dropdown-caret { + visibility: visible; + opacity: 1 + } + } + } + + &:focus, + &:active { + .ci-status-icon-success { + background-color: rgba($gl-success, .1); + } + + .ci-status-icon-failed { + background-color: rgba($gl-danger, .1); + } + + .ci-status-icon-pending, + .ci-status-icon-success_with_warnings { + background-color: rgba($gl-warning, .1); + } + + .ci-status-icon-running { + background-color: rgba($blue-normal, .1); + } + + .ci-status-icon-canceled, + .ci-status-icon-disabled, + .ci-status-icon-not-found { + background-color: rgba($gl-gray, .1); + } + + .ci-status-icon-created, + .ci-status-icon-skipped { + background-color: rgba($gray-darkest, .1); + } + } + + .mini-pipeline-graph-icon-container { + .dropdown-caret { + font-size: 11px; + position: absolute; + top: 5px; + left: 20px; + margin-right: -6px; + z-index: 2; + visibility: hidden; + opacity: 0; + transition: visibility 0.1s, opacity 0.3s linear + } + } } .dropdown-build .build-content { @@ -849,7 +911,7 @@ height: 22px; position: relative; z-index: 2; - transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); + transition: all 0.1s cubic-bezier(0.25, 0, 1, 1); svg { top: -1px; @@ -862,76 +924,6 @@ height: 22px; } -.builds-dropdown { - &:focus, - &:hover { - outline: none; - margin-right: -8px; - - .ci-status-icon { - width: 32px; - padding: 0 8px 0 0; - transition: width 0.2s cubic-bezier(0.25, 0, 1, 1); - - + .dropdown-caret { - display: inline-block; - } - } - } - - &:focus, - &:active { - .ci-status-icon-success { - background-color: rgba($gl-success, .1); - } - - .ci-status-icon-failed { - background-color: rgba($gl-danger, .1); - } - - .ci-status-icon-pending, - .ci-status-icon-success_with_warnings { - background-color: rgba($gl-warning, .1); - } - - .ci-status-icon-running { - background-color: rgba($blue-normal, .1); - } - - .ci-status-icon-canceled, - .ci-status-icon-disabled, - .ci-status-icon-not-found { - background-color: rgba($gl-gray, .1); - } - - .ci-status-icon-created, - .ci-status-icon-skipped { - background-color: rgba($gray-darkest, .1); - } - } - - .mini-pipeline-graph-icon-container { - .ci-status-icon:hover, - .ci-status-icon:focus { - width: 32px; - padding: 0 8px 0 0; - - + .dropdown-caret { - display: inline-block; - } - } - - .dropdown-caret { - font-size: 11px; - position: absolute;; - top: 5px; - left: 20px; - margin-right: -6px; - display: none; - z-index: 2; - } - } -} .terminal-icon { margin-left: 3px; From c95137fce3ecbb6c6d4d7a6425736c365f5e5c2c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 23 Dec 2016 00:36:55 +0000 Subject: [PATCH 191/206] Fix alignment in Safari --- app/assets/stylesheets/pages/pipelines.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d75a5f42c2f..1f3fb002074 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -201,7 +201,7 @@ width: 8px; position: absolute; right: -7px; - bottom: 10px; + top: 10px; border-bottom: 2px solid $border-color; } } @@ -784,7 +784,6 @@ .mini-pipeline-graph { .builds-dropdown { background-color: transparent; - border: none; padding: 0; color: $gl-text-color-light; border: none; From e4ff2a047b8d31684ff6d7d542f87c5f50cc5220 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 23 Dec 2016 00:38:59 +0000 Subject: [PATCH 192/206] Adds CHANGELOG entry --- .../unreleased/26018-mini-pipeline-hover-cross-broswer.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/26018-mini-pipeline-hover-cross-broswer.yml diff --git a/changelogs/unreleased/26018-mini-pipeline-hover-cross-broswer.yml b/changelogs/unreleased/26018-mini-pipeline-hover-cross-broswer.yml new file mode 100644 index 00000000000..501f0b25a21 --- /dev/null +++ b/changelogs/unreleased/26018-mini-pipeline-hover-cross-broswer.yml @@ -0,0 +1,4 @@ +--- +title: Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari +merge_request: 8282 +author: From b7894a7451b5e6802f24690162cdf1fbb3454136 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 23 Dec 2016 09:46:32 +0100 Subject: [PATCH 193/206] Fix errorcode 500 on slash commands Coverage was failing, so this could slip through the cracks --- lib/gitlab/chat_commands/deploy.rb | 5 ++-- lib/gitlab/chat_commands/presenter.rb | 12 +++++----- spec/lib/gitlab/chat_commands/command_spec.rb | 24 +++++++++++++++++++ .../mattermost_slash_commands_service_spec.rb | 2 +- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 6bb854dc080..0f70323810d 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -49,8 +49,9 @@ module Gitlab end def url(subject) - polymorphic_url( - [ subject.project.namespace.becomes(Namespace), subject.project, subject ]) + project = subject.project + + namespace_project_build_url(project.namespace.becomes(Namespace), project, subject) end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index caceaa25391..8930a21f406 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -30,12 +30,12 @@ module Gitlab if subject.is_a?(Gitlab::ChatCommands::Result) show_result(subject) elsif subject.respond_to?(:count) - if subject.many? - multiple_resources(subject) - elsif subject.none? + if subject.none? not_found + elsif subject.one? + single_resource(subject.first) else - single_resource(subject) + multiple_resources(subject) end else single_resource(subject) @@ -71,9 +71,9 @@ module Gitlab end def multiple_resources(resources) - resources.map! { |resource| title(resource) } + titles = resources.map { |resource| title(resource) } - message = header_with_list("Multiple results were found:", resources) + message = header_with_list("Multiple results were found:", titles) ephemeral_response(message) end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index a0ec8884635..a2d84977f58 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -54,6 +54,30 @@ describe Gitlab::ChatCommands::Command, service: true do end end + context 'searching for an issue' do + let(:params) { { text: 'issue search find me' } } + let!(:issue) { create(:issue, project: project, title: 'find me') } + + before do + project.team << [user, :master] + end + + context 'a single issue is found' do + it 'presents the issue' do + expect(subject[:text]).to match(issue.title) + end + end + + context 'multiple issues found' do + let!(:issue2) { create(:issue, project: project, title: "someone find me") } + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(issue.title) + expect(subject[:text]).to match(issue2.title) + end + end + end + context 'when trying to do deployment' do let(:params) { { text: 'deploy staging to production' } } let!(:build) { create(:ci_build, project: project) } diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 672ced68681..c879edddfdd 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -6,7 +6,7 @@ describe MattermostSlashCommandsService, :models do context 'Mattermost API' do let(:project) { create(:empty_project) } let(:service) { project.build_mattermost_slash_commands_service } - let(:user) { create(:user)} + let(:user) { create(:user) } before do Mattermost::Session.base_uri("http://mattermost.example.com") From b63f2794f076a7394c8a000829a632bcffef2b00 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 23 Dec 2016 16:53:56 +0800 Subject: [PATCH 194/206] Ci::Pipeline.latest order by id DESC The name latest implies that it's reverse chronological, and we did expect it that way. https://gitlab.com/gitlab-org/gitlab-ce/issues/25993#note_20429761 >>> ok, I think markglenfletchera is correct in https://gitlab.com/gitlab-com/support-forum/issues/1394#note_20399939 that `Project#latest_successful_builds_for` is giving oldest pipeline rather than latest pipeline. This is a ~regression introduced by !7333 where `order(id: :desc)` was removed causing this. The offending change was: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7333/diffs#b22732e5f39e176c7c719fe485847d0fb0564275_92_108 The confusion was caused by the `latest` name implication, which actually didn't order anything, and I think we should add `order(id: :desc)` to `Ci::Pipeline.latest` otherwise it's confusing that it's not actually ordered. >>> Closes #25993 --- app/models/ci/pipeline.rb | 12 +++++++----- app/models/project.rb | 2 +- .../unreleased/fix-latest-pipeine-ordering.yml | 4 ++++ spec/models/ci/pipeline_spec.rb | 14 ++++++-------- 4 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/fix-latest-pipeine-ordering.yml diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f2f6453b3b9..5494a8da0d9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -93,11 +93,13 @@ module Ci .select("max(#{quoted_table_name}.id)") .group(:ref, :sha) - if ref - where(id: max_id, ref: ref) - else - where(id: max_id) - end + query = if ref + where(id: max_id, ref: ref) + else + where(id: max_id) + end + + query.order(id: :desc) end def self.latest_status(ref = nil) diff --git a/app/models/project.rb b/app/models/project.rb index 26fa20f856d..72fdd4514c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -418,7 +418,7 @@ class Project < ActiveRecord::Base repository.commit(ref) end - # ref can't be HEAD, can only be branch/tag name or SHA + # ref can't be HEAD or SHA, can only be branch/tag name def latest_successful_builds_for(ref = default_branch) latest_pipeline = pipelines.latest_successful_for(ref) diff --git a/changelogs/unreleased/fix-latest-pipeine-ordering.yml b/changelogs/unreleased/fix-latest-pipeine-ordering.yml new file mode 100644 index 00000000000..c7c9885d55a --- /dev/null +++ b/changelogs/unreleased/fix-latest-pipeine-ordering.yml @@ -0,0 +1,4 @@ +--- +title: Fix downloading latest artifact +merge_request: 8286 +author: diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index dc377d15f15..f5d206e0a3e 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -424,20 +424,18 @@ describe Ci::Pipeline, models: true do context 'when no ref is specified' do let(:pipelines) { described_class.latest.all } - it 'returns the latest pipeline for the same ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') - expect(pipelines.map(&:status)). - to contain_exactly('success', 'failed', 'skipped') + it 'returns the latest pipelines for the same ref and different sha' do + expect(pipelines.map(&:sha)).to eq(%w[C B A]) + expect(pipelines.map(&:status)).to eq(%w[skipped failed success]) end end context 'when ref is specified' do let(:pipelines) { described_class.latest('ref').all } - it 'returns the latest pipeline for ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') - expect(pipelines.map(&:status)). - to contain_exactly('success', 'failed') + it 'returns the latest pipelines for ref and different sha' do + expect(pipelines.map(&:sha)).to eq(%w[B A]) + expect(pipelines.map(&:status)).to eq(%w[failed success]) end end end From 13d009ce545271c6d0422a8df61c74028439d8bd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 23 Dec 2016 18:17:25 +0800 Subject: [PATCH 195/206] Prefer oneline and Rubocop prefers ternary operator Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8286/diffs#note_20433402 --- app/models/ci/pipeline.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5494a8da0d9..6894a5763ff 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -93,13 +93,8 @@ module Ci .select("max(#{quoted_table_name}.id)") .group(:ref, :sha) - query = if ref - where(id: max_id, ref: ref) - else - where(id: max_id) - end - - query.order(id: :desc) + relation = ref ? where(ref: ref) : self + relation.where(id: max_id).order(id: :desc) end def self.latest_status(ref = nil) From 1c275a7523e5d891a8d15720386b6119d0cedf41 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 23 Dec 2016 18:30:25 +0800 Subject: [PATCH 196/206] Update description stating that ordering is important Feedback from Grzegorz --- spec/models/ci/pipeline_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f5d206e0a3e..b28da6daabf 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -424,7 +424,7 @@ describe Ci::Pipeline, models: true do context 'when no ref is specified' do let(:pipelines) { described_class.latest.all } - it 'returns the latest pipelines for the same ref and different sha' do + it 'gives the latest pipelines for the same ref and different sha in reverse chronological order' do expect(pipelines.map(&:sha)).to eq(%w[C B A]) expect(pipelines.map(&:status)).to eq(%w[skipped failed success]) end @@ -433,7 +433,7 @@ describe Ci::Pipeline, models: true do context 'when ref is specified' do let(:pipelines) { described_class.latest('ref').all } - it 'returns the latest pipelines for ref and different sha' do + it 'gives the latest pipelines for ref and different sha in reverse chronological order' do expect(pipelines.map(&:sha)).to eq(%w[B A]) expect(pipelines.map(&:status)).to eq(%w[failed success]) end From 332b85d2df466118b62b6599bd18036d691d04ed Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 23 Dec 2016 18:49:40 +0800 Subject: [PATCH 197/206] Update the description because it's more broader Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8286/diffs#note_20435082 --- changelogs/unreleased/fix-latest-pipeine-ordering.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/fix-latest-pipeine-ordering.yml b/changelogs/unreleased/fix-latest-pipeine-ordering.yml index c7c9885d55a..3dbd1ba036a 100644 --- a/changelogs/unreleased/fix-latest-pipeine-ordering.yml +++ b/changelogs/unreleased/fix-latest-pipeine-ordering.yml @@ -1,4 +1,4 @@ --- -title: Fix downloading latest artifact +title: Fix finding the latest pipeline merge_request: 8286 author: From 8f490cbe0e4f316ab2496fb253cd7bf90856e31d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 23 Dec 2016 12:06:45 +0000 Subject: [PATCH 198/206] Fix eslint errors --- app/assets/stylesheets/pages/pipelines.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1f3fb002074..a8392154a44 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -801,7 +801,7 @@ + .dropdown-caret { visibility: visible; - opacity: 1 + opacity: 1; } } } @@ -847,7 +847,7 @@ z-index: 2; visibility: hidden; opacity: 0; - transition: visibility 0.1s, opacity 0.3s linear + transition: visibility 0.1s, opacity 0.3s linear; } } } From 89d3ef38ccc7be722a0906d08b51a48b1c8ff681 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 21 Dec 2016 16:26:35 +0100 Subject: [PATCH 199/206] Schedule at most 100 commits When processing push payloads we now schedule at most the 100 most recent commits, instead of all commits that were in a payload. This prevents one from overloading the system by pushing thousands if not millions of commits in a single go. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/25827 --- app/services/git_push_service.rb | 24 ++++++++++--------- .../process-commit-worker-large-batches.yml | 4 ++++ spec/services/git_push_service_spec.rb | 19 +++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/process-commit-worker-large-batches.yml diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 185556c12cc..6bbc3a9d9ff 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -3,6 +3,9 @@ class GitPushService < BaseService include Gitlab::CurrentSettings include Gitlab::Access + # The N most recent commits to process in a single push payload. + PROCESS_COMMIT_LIMIT = 100 + # This method will be called after each git update # and only if the provided user and project are present in GitLab. # @@ -77,6 +80,16 @@ class GitPushService < BaseService ProjectCacheWorker.perform_async(@project.id, types) end + # Schedules processing of commit messages. + def process_commit_messages + default = is_default_branch? + + push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| + ProcessCommitWorker. + perform_async(project.id, current_user.id, commit.to_hash, default) + end + end + protected def execute_related_hooks @@ -128,17 +141,6 @@ class GitPushService < BaseService end end - # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, - # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. - def process_commit_messages - default = is_default_branch? - - @push_commits.each do |commit| - ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.to_hash, default) - end - end - def build_push_data @push_data ||= Gitlab::DataBuilder::Push.build( @project, diff --git a/changelogs/unreleased/process-commit-worker-large-batches.yml b/changelogs/unreleased/process-commit-worker-large-batches.yml new file mode 100644 index 00000000000..6fa31b62e4a --- /dev/null +++ b/changelogs/unreleased/process-commit-worker-large-batches.yml @@ -0,0 +1,4 @@ +--- +title: Push payloads schedule at most 100 commits, instead of all commits +merge_request: +author: diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index e7624e70725..3303e808a9c 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -604,6 +604,25 @@ describe GitPushService, services: true do end end + describe '#process_commit_messages' do + let(:service) do + described_class.new(project, + user, + oldrev: sample_commit.parent_id, + newrev: sample_commit.id, + ref: 'refs/heads/master') + end + + it 'only schedules a limited number of commits' do + allow(service).to receive(:push_commits). + and_return(Array.new(1000, double(:commit, to_hash: {}))) + + expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times + + service.process_commit_messages + end + end + def execute_service(project, user, oldrev, newrev, ref) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service.execute From b75cec0d1ba499c8a5dd7ab808351ccdfc5ffdd1 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 23 Dec 2016 11:12:30 -0200 Subject: [PATCH 200/206] [ci skip] Fix wrong release date for 8.15.0 on CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd40b82434..acbdd52a67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. -## 8.15.0 (2017-01-22) +## 8.15.0 (2016-12-22) - Whitelist next project names: notes, services. - Use Grape's new Route methods. From 65322905d5b83770598d1cd3963c427ae85bfdd9 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Fri, 23 Dec 2016 16:42:44 +0100 Subject: [PATCH 201/206] Improve spec for resetting incoming email token --- spec/features/issues_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5c958455604..b071fe480e6 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -371,23 +371,25 @@ describe 'Issues', feature: true do describe 'when I want to reset my incoming email token' do let(:project1) { create(:project, namespace: @user.namespace) } - let(:issue) { create(:issue, project: project1) } + let!(:issue) { create(:issue, project: project1) } before do - allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") project1.team << [@user, :master] - project1.issues << issue visit namespace_project_issues_path(@user.namespace, project1) end it 'changes incoming email address token', js: true do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').click - wait_for_ajax - expect(find('input#issue_email').value).not_to eq(previous_token) + expect(page).to have_no_field('issue_email', with: previous_token) + new_token = project1.new_issue_address(@user.reload) + expect(page).to have_field( + 'issue_email', + with: new_token + ) end end From dfef28c920363036584d12cd030e021a789f522e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 23 Dec 2016 15:19:16 -0200 Subject: [PATCH 202/206] Rename `opt` to `opts` on ChatNotificationService#execute --- app/models/project_services/chat_notification_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 475344d80ce..de815ff2bfe 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -51,11 +51,11 @@ class ChatNotificationService < Service channel_name = get_channel_field(object_kind).presence || channel - opt = {} - opt[:channel] = channel_name if channel_name - opt[:username] = username if username + opts = {} + opts[:channel] = channel_name if channel_name + opts[:username] = username if username - notifier = Slack::Notifier.new(webhook, opt) + notifier = Slack::Notifier.new(webhook, opts) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) true From f4a2d33a6795b84a3c7c00de0fb8dbfa6100c037 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <jacob@gitlab.com> Date: Fri, 23 Dec 2016 13:42:42 +0100 Subject: [PATCH 203/206] Monkey-patch StrongParameters for ::UploadedFile --- config/initializers/workhorse_multipart.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb index 3e2f25c354a..8de7140e3d4 100644 --- a/config/initializers/workhorse_multipart.rb +++ b/config/initializers/workhorse_multipart.rb @@ -1,3 +1,19 @@ Rails.application.configure do |config| config.middleware.use(Gitlab::Middleware::Multipart) end + +module Gitlab + module StrongParameterScalars + GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile] + + def permitted_scalar?(value) + super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } + end + end +end + +module ActionController + class Parameters + prepend Gitlab::StrongParameterScalars + end +end From d54d5465967f9473c31a2e9572a4262b671ec6a7 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 23 Dec 2016 15:55:13 -0200 Subject: [PATCH 204/206] Fix rubucop offenses --- app/models/project_services/chat_notification_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index de815ff2bfe..b7ef44c3054 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -49,10 +49,10 @@ class ChatNotificationService < Service return false unless message - channel_name = get_channel_field(object_kind).presence || channel + channel_name = get_channel_field(object_kind).presence || channel opts = {} - opts[:channel] = channel_name if channel_name + opts[:channel] = channel_name if channel_name opts[:username] = username if username notifier = Slack::Notifier.new(webhook, opts) From eb25f232fdd6947189d3ae024f2548178c876154 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Fri, 23 Dec 2016 19:32:15 +0100 Subject: [PATCH 205/206] minor css edits, quicker fade animation for caret, and positioned it slightly down by 1 px --- app/assets/stylesheets/pages/pipelines.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a8392154a44..5f8874289cc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -841,13 +841,13 @@ .dropdown-caret { font-size: 11px; position: absolute; - top: 5px; + top: 6px; left: 20px; margin-right: -6px; z-index: 2; visibility: hidden; opacity: 0; - transition: visibility 0.1s, opacity 0.3s linear; + transition: visibility 0.1s, opacity 0.1s linear; } } } From 21fb690c216f9e40be07229726e106d7016e13fc Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 23 Dec 2016 19:27:00 -0200 Subject: [PATCH 206/206] Update CHANGELOG.md for 8.15.1 [ci skip] --- CHANGELOG.md | 10 ++++++++++ .../unreleased/25905-mr-when-succeeds-dropdown.yml | 4 ---- changelogs/unreleased/25906-title-size.yml | 4 ---- changelogs/unreleased/25961-spec-list-blank.yml | 4 ---- ...-hide-retried-builds-in-pipeline-stage-dropdown.yml | 4 ---- changelogs/unreleased/fix-mattermost-username.yml | 4 ---- changelogs/unreleased/issue_25887.yml | 4 ---- .../unreleased/process-commit-worker-large-batches.yml | 4 ---- 8 files changed, 10 insertions(+), 28 deletions(-) delete mode 100644 changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml delete mode 100644 changelogs/unreleased/25906-title-size.yml delete mode 100644 changelogs/unreleased/25961-spec-list-blank.yml delete mode 100644 changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml delete mode 100644 changelogs/unreleased/fix-mattermost-username.yml delete mode 100644 changelogs/unreleased/issue_25887.yml delete mode 100644 changelogs/unreleased/process-commit-worker-large-batches.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index acbdd52a67c..c57ba82e38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.15.1 (2016-12-23) + +- Push payloads schedule at most 100 commits, instead of all commits. +- Fix Mattermost command creation by specifying username. +- Do not override incoming webhook for mattermost and slack. +- Adds background color for disabled state to merge when succeeds dropdown. !8222 +- Standardises font-size for titles in Issues, Merge Requests and Merge Request widget. !8235 +- Fix Pipeline builds list blank on MR. !8255 +- Do not show retried builds in pipeline stage dropdown. !8260 + ## 8.15.0 (2016-12-22) - Whitelist next project names: notes, services. diff --git a/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml deleted file mode 100644 index 39ce0b66768..00000000000 --- a/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds background color for disabled state to merge when succeeds dropdown -merge_request: 8222 -author: diff --git a/changelogs/unreleased/25906-title-size.yml b/changelogs/unreleased/25906-title-size.yml deleted file mode 100644 index d92d06197e9..00000000000 --- a/changelogs/unreleased/25906-title-size.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Standardises font-size for titles in Issues, Merge Requests and Merge Request widget -merge_request: 8235 -author: diff --git a/changelogs/unreleased/25961-spec-list-blank.yml b/changelogs/unreleased/25961-spec-list-blank.yml deleted file mode 100644 index 835def027a7..00000000000 --- a/changelogs/unreleased/25961-spec-list-blank.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Pipeline builds list blank on MR -merge_request: 8255 -author: diff --git a/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml b/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml deleted file mode 100644 index 66256d7ed0e..00000000000 --- a/changelogs/unreleased/fix-hide-retried-builds-in-pipeline-stage-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not show retried builds in pipeline stage dropdown -merge_request: 8260 -author: diff --git a/changelogs/unreleased/fix-mattermost-username.yml b/changelogs/unreleased/fix-mattermost-username.yml deleted file mode 100644 index ca298e4d008..00000000000 --- a/changelogs/unreleased/fix-mattermost-username.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Mattermost command creation by specifying username -merge_request: -author: diff --git a/changelogs/unreleased/issue_25887.yml b/changelogs/unreleased/issue_25887.yml deleted file mode 100644 index 27299bbc5f9..00000000000 --- a/changelogs/unreleased/issue_25887.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not override incoming webhook for mattermost and slack -merge_request: -author: diff --git a/changelogs/unreleased/process-commit-worker-large-batches.yml b/changelogs/unreleased/process-commit-worker-large-batches.yml deleted file mode 100644 index 6fa31b62e4a..00000000000 --- a/changelogs/unreleased/process-commit-worker-large-batches.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Push payloads schedule at most 100 commits, instead of all commits -merge_request: -author: