From 26c69c5df8bf7800e16a2d12d207f2ae8779fb9e Mon Sep 17 00:00:00 2001 From: winniehell Date: Sat, 5 Nov 2016 12:28:48 +0100 Subject: Display error code for U2F errors (!7305) --- app/assets/javascripts/u2f/authenticate.js | 3 ++- app/assets/javascripts/u2f/error.js | 1 - app/assets/javascripts/u2f/register.js | 3 ++- app/views/u2f/_authenticate.html.haml | 2 +- app/views/u2f/_register.html.haml | 2 +- changelogs/unreleased/remove-u2f-error-logging.yml | 4 ++++ 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/remove-u2f-error-logging.yml diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 35f2b1e2b25..6f0dbca96ca 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -85,7 +85,8 @@ U2FAuthenticate.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index aff605169e4..fab4165efe5 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -7,7 +7,6 @@ this.errorCode = errorCode; this.message = bind(this.message, this); this.httpsDisabled = window.location.protocol !== 'https:'; - console.error("U2F Error Code: " + this.errorCode); } U2FError.prototype.message = function() { diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 22fbf9f3a91..3e0a2d53bd2 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -72,7 +72,8 @@ U2FRegister.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 232ca26c1af..fa998c91f72 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -13,7 +13,7 @@ %script#js-authenticate-u2f-error{ type: "text/template" } %div - %p <%= error_message %> + %p <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-authenticate-u2f-authenticated{ type: "text/template" } diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 8f7b42eb351..fcc33f04237 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -23,7 +23,7 @@ %script#js-register-u2f-error{ type: "text/template" } %div %p - %span <%= error_message %> + %span <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-register-u2f-registered{ type: "text/template" } diff --git a/changelogs/unreleased/remove-u2f-error-logging.yml b/changelogs/unreleased/remove-u2f-error-logging.yml new file mode 100644 index 00000000000..edbe576a976 --- /dev/null +++ b/changelogs/unreleased/remove-u2f-error-logging.yml @@ -0,0 +1,4 @@ +--- +title: Display error code for U2F errors +merge_request: 7305 +author: winniehell -- cgit v1.2.1 From 3c2cb28e84aa231d09bb8e5958b61eb2ef056eb2 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 1 Dec 2016 13:32:05 +0100 Subject: 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: -- cgit v1.2.1 From af5de82c67bd1867e69438b71f67f43d026e4edf Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 2 Dec 2016 00:41:22 +0100 Subject: 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; } } -- cgit v1.2.1 From 1c489296fd43fa45621b2ab6e6982327fb864b56 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Fri, 9 Dec 2016 12:25:29 +0100 Subject: Remove trailing whitespace when generating changelog entry --- bin/changelog | 11 ++++++++--- .../unreleased/nuke-ugly-spaces-in-changelog-generator.yml | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml diff --git a/bin/changelog b/bin/changelog index e07b1ad237a..4c894f8ff5b 100755 --- a/bin/changelog +++ b/bin/changelog @@ -84,12 +84,15 @@ class ChangelogEntry end end + private + def contents - YAML.dump( + yaml_content = YAML.dump( 'title' => title, 'merge_request' => options.merge_request, 'author' => options.author ) + remove_trailing_whitespace(yaml_content) end def write @@ -101,8 +104,6 @@ class ChangelogEntry exec("git commit --amend") end - private - def fail_with(message) $stderr.puts "\e[31merror\e[0m #{message}" exit 1 @@ -160,6 +161,10 @@ class ChangelogEntry def branch_name @branch_name ||= %x{git symbolic-ref --short HEAD}.strip end + + def remove_trailing_whitespace(yaml_content) + yaml_content.gsub(/ +$/, '') + end end if $0 == __FILE__ diff --git a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml new file mode 100644 index 00000000000..fd173031107 --- /dev/null +++ b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml @@ -0,0 +1,4 @@ +--- +title: Remove trailing whitespace when generating changelog entry +merge_request: 7948 +author: -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From e485b3f6ad3c220655e4aa909d93bca7a4ae6afc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 15 Dec 2016 00:28:55 +0800 Subject: Give forbidden if project for the build was deleted Closes #25309 --- lib/ci/api/builds.rb | 19 ++++++------------- lib/ci/api/helpers.rb | 7 +++++-- spec/requests/ci/api/builds_spec.rb | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index ed87a2603e8..3c4cfccb19a 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -41,7 +41,7 @@ module Ci put ":id" do authenticate_runner! build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) - forbidden!('Build has been erased!') if build.erased? + authenticate_build!(build, verify_token: false) update_runner_info @@ -71,9 +71,7 @@ module Ci # PATCH /builds/:id/trace.txt patch ":id/trace.txt" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) - forbidden!('Build has been erased!') if build.erased? + authenticate_build!(build) error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') content_range = request.headers['Content-Range'] @@ -104,8 +102,7 @@ module Ci Gitlab::Workhorse.verify_api_request!(headers) not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) forbidden!('build is not running') unless build.running? if params[:filesize] @@ -142,10 +139,8 @@ module Ci require_gitlab_workhorse! not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) forbidden!('Build is not running!') unless build.running? - forbidden!('Build has been erased!') if build.erased? artifacts_upload_path = ArtifactUploader.artifacts_upload_path artifacts = uploaded_file(:file, artifacts_upload_path) @@ -176,8 +171,7 @@ module Ci # GET /builds/:id/artifacts get ":id/artifacts" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) artifacts_file = build.artifacts_file unless artifacts_file.file_storage? @@ -202,8 +196,7 @@ module Ci # DELETE /builds/:id/artifacts delete ":id/artifacts" do build = Ci::Build.find_by_id(params[:id]) - not_found! unless build - authenticate_build_token!(build) + authenticate_build!(build) build.erase_artifacts! end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index e608f5f6cad..0202b3cf8a3 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -13,8 +13,11 @@ module Ci forbidden! unless current_runner end - def authenticate_build_token!(build) - forbidden! unless build_token_valid?(build) + def authenticate_build!(build, verify_token: true) + not_found! unless build + forbidden! if verify_token && !build_token_valid?(build) + forbidden!('Project has been deleted!') unless build.project + forbidden!('Build has been erased!') if build.erased? end def runner_registration_token_valid? diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 80652129928..d61a9afd12e 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -329,6 +329,25 @@ describe Ci::API::Builds do end end end + + context 'when project for the build has been deleted' do + let(:build) do + create(:ci_build, + :pending, + :trace, + runner_id: runner.id, + pipeline: pipeline) + end + + it 'responds with forbidden' do + expect(response.status).to eq 403 + end + + def initial_patch_the_trace + build.project.update(pending_delete: true) + super + end + end end context 'when Runner makes a force-patch' do -- cgit v1.2.1 From 4ae28cb31d9f0915aac647e1befa61067b6932f1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 17:30:49 +0000 Subject: Changes after review --- app/assets/stylesheets/pages/pipelines.scss | 10 +-- app/views/ci/status/_graph_badge.html.haml | 5 +- spec/features/projects/pipelines/pipeline_spec.rb | 91 +++++++++++++++-------- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d3f39570f11..be22e7bdc79 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -645,14 +645,6 @@ margin-bottom: 0; line-height: 1.2; } - - li:first-child { - padding-top: 6px; - } - - li:last-child { - padding-bottom: 6px; - } } .dropdown-build { @@ -741,4 +733,4 @@ .ci-play-icon { padding: 5px 5px 5px 7px; } -} \ No newline at end of file +} diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index a7e8544e7d4..df1b763e67c 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -3,9 +3,10 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) - klass = "ci-status-icon ci-status-icon-#{status}" +- tooltip_title = "#{subject.name} - #{status.label}" - if status.has_details? - = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status}" } do + = link_to status.details_path, data: { toggle: 'tooltip', title: tooltip_title } do %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name - else @@ -14,6 +15,6 @@ - if status.has_action? = link_to status.action_path, method: status.action_method, - title: "#{subject.name}: #{status.action_title}", class: 'ci-action-icon-container' do + title: tooltip_title, class: 'ci-action-icon-container' do %i.ci-action-icon-wrapper = icon(status.action_icon, class: status.action_class) diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 80a596d34c9..9d43d264bdf 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -39,62 +39,87 @@ describe "Pipelines", feature: true, js: true do end context 'pipeline graph' do - it 'shows a running icon and a cancel action for the running build' do - title = "#{@running.name} - #{@running.status}" - - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-running') - expect(page).to have_content('deploy') + context 'running build' do + it 'shows a running icon and a cancel action for the running build' do + page.within('a[data-title="deploy - running"]') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content('deploy') + end + + page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-ban') - end + it 'should be possible to cancel the running build' do + find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click') + expect(page).not_to have_content('Cancel running') + end end - it 'shows the success icon and a retry action for the successfull build' do - title = "#{@success.name} - #{@success.status}" + context 'success build' do + it 'shows the success icon and a retry action for the successfull build' do + page.within('a[data-title="build - passed"]') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-success') - expect(page).to have_content('build') + page.within('a[data-title="build - passed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + it 'should be possible to retry the success build' do + find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') end end - it 'shows the failed icon and a retry action for the failed build' do - title = "#{@failed.name} - #{@failed.status}" + context 'failed build' do + it 'shows the failed icon and a retry action for the failed build' do + page.within('a[data-title="test - failed"]') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-failed') - expect(page).to have_content('test') + page.within('a[data-title="test - failed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + it 'should be possible to retry the failed build' do + find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') end end - it 'shows the skipped icon and a play action for the manual build' do - title = "#{@manual.name} - #{@manual.status}" + context 'manual build' do + it 'shows the skipped icon and a play action for the manual build' do + page.within('a[data-title="manual build - manual play action"]') do + expect(page).to have_selector('.ci-status-icon-skipped') + expect(page).to have_content('manual') + end - page.within("a[data-title='#{title}']") do - expect(page).to have_selector('.ci-status-icon-skipped') - expect(page).to have_content('manual') + page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-play') + end end - page.within("a[data-title='#{title}'] + .ci-action-icon-container") do - expect(page).to have_selector('.ci-action-icon-container .fa-play') + it 'should be possible to play the manual build' do + find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Play build') end end - it 'shows the success icon and the generic comit status build' do - expect(page).to have_selector('.ci-status-icon-success') - expect(page).to have_content('jenkins') + context 'external build' do + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end end end -- cgit v1.2.1 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: Improve session tests --- lib/mattermost/mattermost.rb | 102 -------------------------------- lib/mattermost/session.rb | 105 +++++++++++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ------------- spec/lib/mattermost/session_spec.rb | 68 +++++++++++++++++++++ 4 files changed, 173 insertions(+), 144 deletions(-) delete mode 100644 lib/mattermost/mattermost.rb create mode 100644 lib/mattermost/session.rb 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/mattermost.rb deleted file mode 100644 index 84d016bb197..00000000000 --- a/lib/mattermost/mattermost.rb +++ /dev/null @@ -1,102 +0,0 @@ -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/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..d14121c91a0 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,105 @@ +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 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 + end + + def with_session + raise NoSessionError unless create + result = yield + destroy + + result + 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 ||= begin + 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 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 -- cgit v1.2.1 From 1235d96e973258b9c968049945426cb28bea9998 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 12:10:56 +0000 Subject: Add dropdown statuses in mini-pipeline graph --- .../projects/ci/pipelines/_pipeline.html.haml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 3f05a21990f..f17a0d668b7 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,10 +43,26 @@ %td.stage-cell - pipeline.stages.each do |stage| - if stage.status + - status = ci_label_for_status(stage.detailed_status) + - hasMultipleBuilds = stage.statuses.count > 1 - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do - = ci_icon_for_status(stage.status) + + - if status + .stage-container + - if hasMultipleBuilds + .dropdown.inline + %a.dropdown-toggle{id: "dropdown-#{stage.name}", "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false" } + = ci_icon_for_status(stage.detailed_status) + %span.caret + .dropdown-menu.grouped-pipeline-dropdown{"aria-labelledby"=> "dropdown-#{stage.name}"} + .arrow + %ul + - stage.statuses.each do |status| + %li + status + - else + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do + = ci_icon_for_status(stage.detailed_status) %td - if pipeline.duration -- cgit v1.2.1 From 01876eccad2bbc9ea2f35cb27c41b7373b20f26b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 13:54:23 +0000 Subject: Initial commit --- app/assets/stylesheets/framework/icons.scss | 24 ++++++++++++++++++++ app/assets/stylesheets/pages/pipelines.scss | 26 ++++++++++++++++++++++ .../projects/ci/pipelines/_pipeline.html.haml | 7 +++--- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 226bd2ead31..88b391ef2df 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -4,6 +4,10 @@ svg { fill: $gl-success; } + + .builds-dropdown-caret { + color: $gl-success; + } } .ci-status-icon-failed { @@ -12,6 +16,10 @@ svg { fill: $gl-danger; } + + .builds-dropdown-caret { + color: $gl-danger; + } } .ci-status-icon-pending, @@ -21,6 +29,10 @@ svg { fill: $gl-warning; } + + .builds-dropdown-caret { + color: $gl-warning; + } } .ci-status-icon-running { @@ -29,6 +41,10 @@ svg { fill: $blue-normal; } + + .builds-dropdown-caret { + color: $blue-normal; + } } .ci-status-icon-canceled, @@ -39,6 +55,10 @@ svg { fill: $gl-gray; } + + .builds-dropdown-caret { + color: $gl-gray; + } } .ci-status-icon-created, @@ -48,4 +68,8 @@ svg { fill: $gray-darkest; } + + .builds-dropdown-caret { + color: $gray-darkest; + } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index be22e7bdc79..50e8165a017 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -734,3 +734,29 @@ padding: 5px 5px 5px 7px; } } + +/** + * Builds dropdown in mini pipeline + */ +.builds-dropdown { + border: none; + background: transparent; + padding: 0; + font-size: inherit; + border-radius: 0; + cursor: pointer; + + .dropdown-caret { + display: none; + position: absolute; + top: 3px; + right: 6px; + font-size: 10px; + } + + &:hover { + .dropdown-caret { + display: block; + } + } +} diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index f17a0d668b7..931dd9d3a71 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -51,15 +51,16 @@ .stage-container - if hasMultipleBuilds .dropdown.inline - %a.dropdown-toggle{id: "dropdown-#{stage.name}", "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false" } + %a.dropdown-toggle.builds-dropdown{id: "dropdown-#{stage.name}", title: tooltip, class: "has-tooltip ci-status-icon-#{stage.status}", "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false"} = ci_icon_for_status(stage.detailed_status) - %span.caret + = icon('caret-down', class: 'dropdown-caret') .dropdown-menu.grouped-pipeline-dropdown{"aria-labelledby"=> "dropdown-#{stage.name}"} .arrow %ul - stage.statuses.each do |status| %li - status + = ci_icon_for_status(status) + -# =render 'ci/status/icon_with_name_and_action', subject: status - else = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do = ci_icon_for_status(stage.detailed_status) -- cgit v1.2.1 From fa4d41bf1836755cbf1f28af1d7841dcd81efeb8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 17:14:10 +0000 Subject: Render with new partials --- .../projects/ci/pipelines/_pipeline.html.haml | 37 +++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 931dd9d3a71..e82faf4f6d3 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,27 +43,28 @@ %td.stage-cell - pipeline.stages.each do |stage| - if stage.status - - status = ci_label_for_status(stage.detailed_status) + - detailed_status = stage.detailed_status(current_user) + - details_path = detailed_status.details_path if detailed_status.has_details? + - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status}" - hasMultipleBuilds = stage.statuses.count > 1 - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - - if status - .stage-container - - if hasMultipleBuilds - .dropdown.inline - %a.dropdown-toggle.builds-dropdown{id: "dropdown-#{stage.name}", title: tooltip, class: "has-tooltip ci-status-icon-#{stage.status}", "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false"} - = ci_icon_for_status(stage.detailed_status) - = icon('caret-down', class: 'dropdown-caret') - .dropdown-menu.grouped-pipeline-dropdown{"aria-labelledby"=> "dropdown-#{stage.name}"} - .arrow - %ul - - stage.statuses.each do |status| - %li - = ci_icon_for_status(status) - -# =render 'ci/status/icon_with_name_and_action', subject: status - - else - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do - = ci_icon_for_status(stage.detailed_status) + .stage-container + - if hasMultipleBuilds + .dropdown.inline.build-content + %button.dropdown-menu-toggle.has-tooltip{id: "dropdown-#{stage.name}", title: tooltip, class: klass, "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false"} + = custom_icon(detailed_status.icon) + = icon('caret-down', class: 'dropdown-caret') + .dropdown-menu.grouped-pipeline-dropdown{"aria-labelledby"=> "dropdown-#{stage.name}"} + .arrow + %ul + - stage.statuses.each do |status| + %li + = render 'ci/status/graph_badge', subject: status + - else + - if details_path + = link_to details_path, class: klass, title: tooltip do + = custom_icon(detailed_status.icon) %td - if pipeline.duration -- cgit v1.2.1 From 708125853117916ce3eeb809e5bb7518c8e5e3d8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 18:34:56 +0000 Subject: Dropdown with arrow --- app/assets/stylesheets/pages/pipelines.scss | 256 ++++++++++++--------- .../projects/ci/pipelines/_pipeline.html.haml | 15 +- 2 files changed, 153 insertions(+), 118 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 50e8165a017..df88e7b5925 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -576,17 +576,14 @@ } } - .ci-status-text { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; - display: inline-block; - position: relative; - font-weight: 100; + // Position in the pipeline graph + .grouped-pipeline-dropdown { + right: -206px; + top: -11px; } +} +<<<<<<< HEAD .dropdown-menu-toggle { background-color: transparent; border: none; @@ -594,110 +591,81 @@ color: $gl-text-color-light; white-space: normal; overflow: visible; +======= +.dropdown-counter-badge { + float: right; + color: $border-color; + font-weight: 100; + font-size: 15px; + margin-right: 2px; +} +>>>>>>> 5ba6f0d... Dropdown with arrow - &:focus { - outline: none; - } +.grouped-pipeline-dropdown { + padding: 0; + width: 191px; + left: auto; + right: -206px; + top: -11px; + box-shadow: 0 1px 5px $black-transparent; - &:hover { - color: $gl-text-color; + a { + display: inline-block; - .dropdown-counter-badge { - color: $gl-text-color; - } + &:hover { + background-color: $stage-hover-bg; } } - .dropdown-counter-badge { - float: right; - clear: right; - color: $border-color; - font-weight: 100; - font-size: 15px; - margin-right: 2px; - } - - .grouped-pipeline-dropdown { - padding: 0; - width: 191px; - left: auto; - right: -206px; - top: -11px; - box-shadow: 0 1px 5px $black-transparent; - - a { - display: inline-block; + ul { + max-height: 245px; + overflow: auto; + margin: 5px 0; - &:hover { - background-color: $stage-hover-bg; - } + li { + padding-top: 2px; + margin: 0 5px; } - ul { - max-height: 245px; - overflow: auto; - margin: 5px 0; - - li { - margin: 0 5px; - padding-left: 0; - padding-bottom: 0; - margin-bottom: 0; - line-height: 1.2; - } + li:first-child { + padding-top: 6px; } - .dropdown-build { - color: $gl-text-color-light; - - a.ci-action-icon-container { - padding: 0; - font-size: 11px; - float: right; - margin-top: 4px; - display: inline-block; - position: relative; + li:last-child { + padding-bottom: 6px; + } + } +} - i { - font-size: 11px; - margin-top: 0; - } - } +.ci-status-text { + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + display: inline-block; + position: relative; + font-weight: 100; +} - &:hover { - background-color: $stage-hover-bg; - border-radius: 3px; - color: $gl-text-color; - } - .ci-action-icon-container { - i { - width: 25px; - height: 25px; +.dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-light; + flex-grow: 1; - &::before { - top: 1px; - left: 1px; - } - } - } - .stage { - max-width: 100px; - width: 100px; - } + &:focus { + outline: none; + } - .ci-status-icon svg { - height: 18px; - width: 18px; - } + &:hover { + color: $gl-text-color; - .ci-status-text { - max-width: 95px; - padding-bottom: 3px; - position: relative; - top: 3px; - } + .dropdown-counter-badge { + color: $gl-text-color; } } } @@ -735,28 +703,94 @@ } } +.dropdown-build { + color: $gl-text-color-light; + + a.ci-action-icon-container { + padding: 0; + font-size: 11px; + float: right; + margin-top: 5px; + + i { + font-size: 11px; + margin-top: 0; + } + } + + &:hover { + background-color: $stage-hover-bg; + border-radius: 3px; + color: $gl-text-color; + } + + .ci-action-icon-container { + i { + width: 25px; + height: 25px; + + &:before{ + top: 1px; + left: 1px; + } + } + } + + .stage { + max-width: 100px; + width: 100px; + } + + .ci-status-icon svg { + height: 18px; + width: 18px; + } + + .ci-status-text { + max-width: 95px; + } +} + /** * Builds dropdown in mini pipeline */ -.builds-dropdown { - border: none; - background: transparent; - padding: 0; - font-size: inherit; - border-radius: 0; - cursor: pointer; +.mini-pipeline-graph { + .builds-dropdown { + background-color: transparent; + border: none; + padding: 0; + color: #8c8c8c; + flex-grow: 1; + } - .dropdown-caret { - display: none; - position: absolute; - top: 3px; - right: 6px; - font-size: 10px; + .grouped-pipeline-dropdown { + right: -172px; + top: 23px; } - &:hover { - .dropdown-caret { - display: block; + .arrow-up { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: -6px; + left: 2px; + border-width: 0 5px 6px 5px; + } + + &::before { + border-width: 0 5px 5px 5px; + border-bottom-color: $border-color; + } + + &::after { + margin-top: 1px; + border-bottom-color: $white-light; } } } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index e82faf4f6d3..9ecdb6269b9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -49,17 +49,18 @@ - hasMultipleBuilds = stage.statuses.count > 1 - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - .stage-container + .stage-container.mini-pipeline-graph - if hasMultipleBuilds .dropdown.inline.build-content - %button.dropdown-menu-toggle.has-tooltip{id: "dropdown-#{stage.name}", title: tooltip, class: klass, "data-toggle"=> "dropdown", "aria-haspopup"=> "true", "aria-expanded" => "false"} - = custom_icon(detailed_status.icon) - = icon('caret-down', class: 'dropdown-caret') - .dropdown-menu.grouped-pipeline-dropdown{"aria-labelledby"=> "dropdown-#{stage.name}"} - .arrow + %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } + %span{ class: klass } + = ci_icon_for_status(detailed_status.icon) + %span= icon('caret-down', class: 'dropdown-caret') + .dropdown-menu.grouped-pipeline-dropdown + .arrow-up %ul - stage.statuses.each do |status| - %li + %li.dropdown-build = render 'ci/status/graph_badge', subject: status - else - if details_path -- cgit v1.2.1 From 222eae7b7b17da923b0291c70ec2b6a4101186e6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 18:41:21 +0000 Subject: Dropdown li style --- app/assets/stylesheets/pages/pipelines.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index df88e7b5925..b1575d5213d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -192,10 +192,6 @@ border-bottom: 2px solid $border-color; } } - - a { - display: block; - } } } @@ -759,8 +755,7 @@ background-color: transparent; border: none; padding: 0; - color: #8c8c8c; - flex-grow: 1; + color: $gl-text-color-light; } .grouped-pipeline-dropdown { @@ -768,6 +763,10 @@ top: 23px; } + .grouped-pipeline-dropdown a { + color: $gl-text-color-light; + } + .arrow-up { &::before, &::after { -- cgit v1.2.1 From 9431fcb31c22d4d9bde7366d930264cc1f5e03a6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 14 Dec 2016 23:47:04 +0000 Subject: style dropdown --- app/assets/stylesheets/pages/pipelines.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b1575d5213d..e0431e26769 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -756,6 +756,10 @@ border: none; padding: 0; color: $gl-text-color-light; + border: none; + background: transparent; + padding: 0; + margin: 0; } .grouped-pipeline-dropdown { -- cgit v1.2.1 From 5ce5e7a0e28062dd6b4a4195f9befe5017dd75ff Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 00:17:40 +0000 Subject: Removes duplicate declaration --- app/assets/stylesheets/pages/pipelines.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index e0431e26769..f98bf73756f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -757,9 +757,7 @@ padding: 0; color: $gl-text-color-light; border: none; - background: transparent; - padding: 0; - margin: 0; + margin: 0; } .grouped-pipeline-dropdown { -- cgit v1.2.1 From 7bfe87242d9d897cdc68eb0df90345f2ec497c36 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 11:58:25 +0000 Subject: Add new borderless icons --- app/views/shared/icons/_icon_status_canceled_borderless.svg | 1 + app/views/shared/icons/_icon_status_created_borderless.svg | 1 + app/views/shared/icons/_icon_status_failed_borderless.svg | 1 + app/views/shared/icons/_icon_status_manual_borderless.svg | 1 + app/views/shared/icons/_icon_status_pending_borderless.svg | 1 + app/views/shared/icons/_icon_status_running_borderless.svg | 1 + app/views/shared/icons/_icon_status_skipped_borderless.svg | 1 + app/views/shared/icons/_icon_status_success_borderless.svg | 1 + app/views/shared/icons/_icon_status_warning_borderless.svg | 1 + 9 files changed, 9 insertions(+) create mode 100644 app/views/shared/icons/_icon_status_canceled_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_created_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_failed_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_manual_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_pending_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_running_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_skipped_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_success_borderless.svg create mode 100644 app/views/shared/icons/_icon_status_warning_borderless.svg diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg new file mode 100644 index 00000000000..bf7fb29185f --- /dev/null +++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg new file mode 100644 index 00000000000..1810d023be8 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg new file mode 100644 index 00000000000..b7022350c74 --- /dev/null +++ b/app/views/shared/icons/_icon_status_failed_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg new file mode 100644 index 00000000000..5eec665688b --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg new file mode 100644 index 00000000000..8d66e9e6c9c --- /dev/null +++ b/app/views/shared/icons/_icon_status_pending_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg new file mode 100644 index 00000000000..2757a168ed5 --- /dev/null +++ b/app/views/shared/icons/_icon_status_running_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg new file mode 100644 index 00000000000..fb3e930b3cb --- /dev/null +++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg new file mode 100644 index 00000000000..8ee5be7ab78 --- /dev/null +++ b/app/views/shared/icons/_icon_status_success_borderless.svg @@ -0,0 +1 @@ + diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg new file mode 100644 index 00000000000..7b061624521 --- /dev/null +++ b/app/views/shared/icons/_icon_status_warning_borderless.svg @@ -0,0 +1 @@ + -- cgit v1.2.1 From 83ebc97ba76abc8375c147565603a14a18223649 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 12:11:14 +0000 Subject: Adds new partial for borderless mini graph icons --- app/views/ci/status/_mini_graph_badge.html.haml | 6 ++++++ app/views/projects/ci/pipelines/_pipeline.html.haml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 app/views/ci/status/_mini_graph_badge.html.haml diff --git a/app/views/ci/status/_mini_graph_badge.html.haml b/app/views/ci/status/_mini_graph_badge.html.haml new file mode 100644 index 00000000000..6031339a802 --- /dev/null +++ b/app/views/ci/status/_mini_graph_badge.html.haml @@ -0,0 +1,6 @@ +- status = subject.detailed_status(current_user) +- icon = "#{status.icon}_borderless" +- klass = "ci-status-icon ci-status-icon-#{status}" + +%span.mini-pipeline-graph-icon-container + %span{ class: klass }= custom_icon(icon) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 9ecdb6269b9..d664d925df9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -54,7 +54,7 @@ .dropdown.inline.build-content %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } %span{ class: klass } - = ci_icon_for_status(detailed_status.icon) + = render 'ci/status/mini_graph_badge', subject: stage %span= icon('caret-down', class: 'dropdown-caret') .dropdown-menu.grouped-pipeline-dropdown .arrow-up @@ -65,7 +65,7 @@ - else - if details_path = link_to details_path, class: klass, title: tooltip do - = custom_icon(detailed_status.icon) + = render 'ci/status/mini_graph_badge', subject: stage %td - if pipeline.duration -- cgit v1.2.1 From 6fde0e094d5e2fb3edea3228ef8f7f193fc4f067 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 13:22:39 +0000 Subject: CSS: dropdown on hover --- app/assets/stylesheets/pages/pipelines.scss | 52 ++++++++++++++++++++++ app/views/ci/status/_mini_graph_badge.html.haml | 3 +- .../projects/ci/pipelines/_pipeline.html.haml | 8 ++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f98bf73756f..105c3fc3e7c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -22,6 +22,7 @@ .table.ci-table { min-width: 1200px; + table-layout: fixed; .pipeline-id { color: $black; @@ -795,3 +796,54 @@ } } } + +.mini-pipeline-graph-icon-container .ci-status-icon { + display: inline-block; + border: 1px solid; + border-radius: 20px; + margin-right: 1px; + width: 20px; + height: 20px; + position: relative; + + svg { + top: -1px; + position: relative; + } +} + +.builds-dropdown { + &:focus { + margin-right: -6px; + + .ci-status-icon { + width: 27px; + padding: 0px 6px 0px 0px; + + + .dropdown-caret { + display: inline-block; + } + } + } + + .mini-pipeline-graph-icon-container { + .ci-status-icon:hover, + .ci-status-icon:focus { + width: 27px; + padding: 0px 6px 0px 0px; + + + .dropdown-caret { + display: inline-block; + } + } + + .dropdown-caret { + font-size: 12px; + position: relative; + top: 3px; + left: -11px; + margin-right: -6px; + display: none; + } + } +} diff --git a/app/views/ci/status/_mini_graph_badge.html.haml b/app/views/ci/status/_mini_graph_badge.html.haml index 6031339a802..34e07e75ae8 100644 --- a/app/views/ci/status/_mini_graph_badge.html.haml +++ b/app/views/ci/status/_mini_graph_badge.html.haml @@ -2,5 +2,4 @@ - icon = "#{status.icon}_borderless" - klass = "ci-status-icon ci-status-icon-#{status}" -%span.mini-pipeline-graph-icon-container - %span{ class: klass }= custom_icon(icon) +%span{ class: klass }= custom_icon(icon) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index d664d925df9..0651447f616 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -54,8 +54,9 @@ .dropdown.inline.build-content %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } %span{ class: klass } - = render 'ci/status/mini_graph_badge', subject: stage - %span= icon('caret-down', class: 'dropdown-caret') + %span.mini-pipeline-graph-icon-container + = render 'ci/status/mini_graph_badge', subject: stage + = icon('caret-down', class: 'dropdown-caret') .dropdown-menu.grouped-pipeline-dropdown .arrow-up %ul @@ -65,7 +66,8 @@ - else - if details_path = link_to details_path, class: klass, title: tooltip do - = render 'ci/status/mini_graph_badge', subject: stage + %span.mini-pipeline-graph-icon-container + = render 'ci/status/mini_graph_badge', subject: stage %td - if pipeline.duration -- cgit v1.2.1 From e36b00e7137a7fd024ba4639ac6cb077edd2618a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 14:46:28 +0000 Subject: CSS - Adds background color. Removes unused CSS Fix scss linter errors Adds changelog entry Increase icons size. Adds transition animation Fix jumping icon. Reduce icon size Fix columns width Changes after review Fix transition --- app/assets/stylesheets/framework/icons.scss | 24 ----- app/assets/stylesheets/pages/pipelines.scss | 116 ++++++++++++++------- app/views/projects/commit/_pipelines_list.haml | 14 +-- app/views/projects/pipelines/index.html.haml | 12 +-- .../unreleased/19703-direct-link-pipelines.yml | 4 + 5 files changed, 93 insertions(+), 77 deletions(-) create mode 100644 changelogs/unreleased/19703-direct-link-pipelines.yml diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 88b391ef2df..226bd2ead31 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -4,10 +4,6 @@ svg { fill: $gl-success; } - - .builds-dropdown-caret { - color: $gl-success; - } } .ci-status-icon-failed { @@ -16,10 +12,6 @@ svg { fill: $gl-danger; } - - .builds-dropdown-caret { - color: $gl-danger; - } } .ci-status-icon-pending, @@ -29,10 +21,6 @@ svg { fill: $gl-warning; } - - .builds-dropdown-caret { - color: $gl-warning; - } } .ci-status-icon-running { @@ -41,10 +29,6 @@ svg { fill: $blue-normal; } - - .builds-dropdown-caret { - color: $blue-normal; - } } .ci-status-icon-canceled, @@ -55,10 +39,6 @@ svg { fill: $gl-gray; } - - .builds-dropdown-caret { - color: $gl-gray; - } } .ci-status-icon-created, @@ -68,8 +48,4 @@ svg { fill: $gray-darkest; } - - .builds-dropdown-caret { - color: $gray-darkest; - } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 105c3fc3e7c..33d3a800e7c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -28,12 +28,16 @@ color: $black; } - .branch-commit { - width: 30%; + .pipeline-date, + .pipeline-status { + width: 10%; + } - .branch-name { - max-width: 195px; - } + .pipeline-info, + .pipeline-commit, + .pipeline-actions, + .pipeline-stages { + width: 20%; } } } @@ -107,7 +111,7 @@ .branch-name { font-weight: bold; - max-width: 150px; + max-width: 120px; overflow: hidden; display: inline-block; white-space: nowrap; @@ -133,7 +137,7 @@ .commit-title { margin-top: 4px; - max-width: 300px; + max-width: 225px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -580,15 +584,6 @@ } } -<<<<<<< HEAD - .dropdown-menu-toggle { - background-color: transparent; - border: none; - padding: 0; - color: $gl-text-color-light; - white-space: normal; - overflow: visible; -======= .dropdown-counter-badge { float: right; color: $border-color; @@ -596,7 +591,6 @@ font-size: 15px; margin-right: 2px; } ->>>>>>> 5ba6f0d... Dropdown with arrow .grouped-pipeline-dropdown { padding: 0; @@ -707,7 +701,9 @@ padding: 0; font-size: 11px; float: right; - margin-top: 5px; + margin-top: 4px; + display: inline-block; + position: relative; i { font-size: 11px; @@ -726,7 +722,7 @@ width: 25px; height: 25px; - &:before{ + &::before { top: 1px; left: 1px; } @@ -782,11 +778,11 @@ border-style: solid; top: -6px; left: 2px; - border-width: 0 5px 6px 5px; + border-width: 0 5px 6px; } &::before { - border-width: 0 5px 5px 5px; + border-width: 0 5px 5px; border-bottom-color: $border-color; } @@ -797,53 +793,93 @@ } } +/** + * Icons in mini pipeline graph + */ .mini-pipeline-graph-icon-container .ci-status-icon { - display: inline-block; - border: 1px solid; - border-radius: 20px; - margin-right: 1px; - width: 20px; - height: 20px; - position: relative; + display: inline-block; + border: 1px solid; + border-radius: 20px; + margin-right: 1px; + width: 20px; + height: 20px; + position: relative; + z-index: 2; + transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); - svg { - top: -1px; - position: relative; - } + svg { + top: -1px; + } } .builds-dropdown { &:focus { - margin-right: -6px; + outline: none; + margin-right: -8px; .ci-status-icon { - width: 27px; - padding: 0px 6px 0px 0px; + width: 28px; + padding: 0 8px 0 0; + transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); + .dropdown-caret { - display: inline-block; + 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: 27px; - padding: 0px 6px 0px 0px; + width: 28px; + padding: 0 8px 0 0; + transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); + .dropdown-caret { - display: inline-block; + display: inline-block; } } .dropdown-caret { - font-size: 12px; + font-size: 11px; position: relative; top: 3px; left: -11px; margin-right: -6px; display: none; + z-index: 2; } } } diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 7f42fde0fea..0c2f45c6035 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -5,11 +5,11 @@ - else .table-holder %table.table.ci-table - %tbody - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th + %thead + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 030cd8ef78f..4d009871f0d 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -44,12 +44,12 @@ .table-holder %table.table.ci-table %thead - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th.hidden-xs + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions.hidden-xs = render @pipelines, commit_sha: true, stage: true, allow_retry: true = paginate @pipelines, theme: 'gitlab' diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml new file mode 100644 index 00000000000..d846ad41e0f --- /dev/null +++ b/changelogs/unreleased/19703-direct-link-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Adds Direct link from pipeline list to builds +merge_request: 8097 +author: -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From 7b664858ff33832cf62fe56322be0cfe04cbbc7d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 21 Sep 2016 19:55:43 +0800 Subject: Add docs for pipeline duration --- doc/ci/pipelines.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 03b9c4bb444..f91b9d350f7 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -36,6 +36,37 @@ Clicking on a pipeline will show the builds that were run for that pipeline. Clicking on an individual build will show you its build trace, and allow you to cancel the build, retry it, or erase the build trace. +## How the pipeline duration is calculated + +Total running time for a given pipeline would exclude retries and pending +(queue) time. We could reduce this problem down to finding the union of +periods. + +So each job would be represented as a `Period`, which consists of +`Period#first` as when the job started and `Period#last` as when the +job was finished. A simple example here would be: + +* A (1, 3) +* B (2, 4) +* C (6, 7) + +Here A begins from 1, and ends to 3. B begins from 2, and ends to 4. +C begins from 6, and ends to 7. Visually it could be viewed as: + +``` +0 1 2 3 4 5 6 7 + AAAAAAA + BBBBBBB + CCCC +``` + +The union of A, B, and C would be (1, 4) and (6, 7), therefore the +total running time should be: + +``` +(4 - 1) + (7 - 6) => 4 +``` + ## Badges Build status and test coverage report badges are available. You can find their -- cgit v1.2.1 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: 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 -- cgit v1.2.1 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: Improve session tests --- lib/mattermost/mattermost.rb | 102 -------------------------------- lib/mattermost/session.rb | 104 +++++++++++++++++++++++++++++++++ spec/lib/mattermost/mattermost_spec.rb | 42 ------------- spec/lib/mattermost/session_spec.rb | 68 +++++++++++++++++++++ 4 files changed, 172 insertions(+), 144 deletions(-) delete mode 100644 lib/mattermost/mattermost.rb create mode 100644 lib/mattermost/session.rb 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/mattermost.rb deleted file mode 100644 index 84d016bb197..00000000000 --- a/lib/mattermost/mattermost.rb +++ /dev/null @@ -1,102 +0,0 @@ -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/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..81964666757 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,104 @@ +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 Session + 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 + result = yield + destroy + + result + 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 ||= begin + 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 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 -- cgit v1.2.1 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: Base work for auto config MM slash commands --- app/controllers/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 | 73 ++++---- 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, 259 insertions(+), 147 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' - - .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) - - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - %p.light - = service_event_description(event) - - - @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 + - if @service.auto_config? + + - 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 + + - field = @service.event_field(event) + + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + + %p.light + = service_event_description(event) + + - @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/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 -- cgit v1.2.1 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: Add MattermostController --- app/controllers/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 -- cgit v1.2.1 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: Add auto configure of commands --- app/controllers/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 -- cgit v1.2.1 From 9046b583ff61d74c8e481053d150da924d129ad8 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 16 Dec 2016 12:51:12 +0100 Subject: 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 -- cgit v1.2.1 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: Add tests for auto configure slash commands --- app/controllers/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 | 73 +++++++++++----------- 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 | 19 +++--- .../mattermost_slash_commands_service_spec.rb | 31 +++++++++ 13 files changed, 123 insertions(+), 104 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? - - - 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 - - - field = @service.event_field(event) - - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - %p.light - = service_event_description(event) - - - @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 + - 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 + + - field = @service.event_field(event) + + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + + %p.light + = service_event_description(event) + + - @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/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 + + private - response.parsed_response['token'] + 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 '.team_admin' do + let(:init_load) do + JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_initial_load.json'))) + end - 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) + allow(described_class).to receive(:initial_load).and_return(init_load) end - xit 'gets the teams' do - allow(session).to receive(:with_session) { yield } + it 'gets the teams' do + expect(described_class.team_admin.count).to be(2) + end - expect(described_class.all).to eq(result) + 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 -- cgit v1.2.1 From 0d3e24358b88ce41848c97f3ac37bc813074f260 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 30 Nov 2016 15:51:48 +0100 Subject: Create Slack Slash command service --- Gemfile | 2 +- Gemfile.lock | 4 +- app/models/project.rb | 1 + .../slack_slash_commands_service.rb | 49 ++++++++ app/models/service.rb | 3 +- .../mattermost_slash_commands/_help.html.haml | 2 +- changelogs/unreleased/zj-slack-slash-commands.yml | 4 + lib/gitlab/chat_commands/deploy.rb | 2 +- lib/gitlab/chat_commands/help.rb | 28 +++++ lib/gitlab/chat_commands/presenters/mattermost.rb | 131 +++++++++++++++++++++ lib/mattermost/presenter.rb | 131 --------------------- spec/lib/gitlab/chat_commands/command_spec.rb | 26 +++- .../chat_message/build_message_spec.rb | 8 +- .../chat_message/issue_message_spec.rb | 10 +- .../chat_message/merge_message_spec.rb | 12 +- .../chat_message/note_message_spec.rb | 22 ++-- .../chat_message/push_message_spec.rb | 26 ++-- .../chat_message/wiki_page_message_spec.rb | 8 +- 18 files changed, 287 insertions(+), 182 deletions(-) create mode 100644 app/models/project_services/slack_slash_commands_service.rb create mode 100644 changelogs/unreleased/zj-slack-slash-commands.yml create mode 100644 lib/gitlab/chat_commands/help.rb create mode 100644 lib/gitlab/chat_commands/presenters/mattermost.rb delete mode 100644 lib/mattermost/presenter.rb diff --git a/Gemfile b/Gemfile index 5eb8c32b168..a59b874248b 100644 --- a/Gemfile +++ b/Gemfile @@ -170,7 +170,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'gemnasium-gitlab-service', '~> 0.2' # Slack integration -gem 'slack-notifier', '~> 1.2.0' +gem 'slack-notifier', '~> 1.5.1' # Asana integration gem 'asana', '~> 0.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 23e45ddc16f..f33b171e1d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -687,7 +687,7 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) - slack-notifier (1.2.1) + slack-notifier (1.5.1) slop (3.6.0) spinach (0.8.10) colorize @@ -957,7 +957,7 @@ DEPENDENCIES sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) - slack-notifier (~> 1.2.0) + slack-notifier (~> 1.5.1) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) spring (~> 1.7.0) diff --git a/app/models/project.rb b/app/models/project.rb index 5d092ca42c2..7f3a4debfc6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -96,6 +96,7 @@ class Project < ActiveRecord::Base has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_notification_service, dependent: :destroy + has_one :slack_slash_commands_service, dependent: :destroy has_one :slack_notification_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb new file mode 100644 index 00000000000..9c66f95eef9 --- /dev/null +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -0,0 +1,49 @@ +class SlackSlashCommandsService < ChatService + include TriggersHelper + + prop_accessor :token + + def can_test? + false + end + + def title + 'Slack Slash Command' + end + + def description + "Perform common operations on GitLab in Slack" + end + + def to_param + 'slack_slash_commands' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' } + ] + end + + def trigger(params) + return nil unless valid_token?(params[:token]) + + user = find_chat_user(params) + unless user + url = authorize_chat_name_url(params) + return Gitlab::ChatCommands::Presenters::Access.new(url).authorize + end + + Gitlab::ChatCommands::Command.new(project, user, params).execute + end + + private + + def find_chat_user(params) + ChatNames::FindUserService.new(self, params).execute + end + + def authorize_chat_name_url(params) + ChatNames::AuthorizeUserService.new(self, params).execute + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 0bbab078cf6..8abd8e73e43 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -216,11 +216,12 @@ class Service < ActiveRecord::Base jira kubernetes mattermost_slash_commands + mattermost_notification pipelines_email pivotaltracker pushover redmine - mattermost_notification + slack_slash_commands slack_notification teamcity ] 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..2bdd4cd148c 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,4 +1,4 @@ -- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" +- pretty_path_with_namespace = "#{@project.namespace ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" - run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" .well diff --git a/changelogs/unreleased/zj-slack-slash-commands.yml b/changelogs/unreleased/zj-slack-slash-commands.yml new file mode 100644 index 00000000000..9f4c8681ad0 --- /dev/null +++ b/changelogs/unreleased/zj-slack-slash-commands.yml @@ -0,0 +1,4 @@ +--- +title: Refactor presenters ChatCommands +merge_request: 7846 +author: diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 0eed1fce0dc..6bb854dc080 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -4,7 +4,7 @@ module Gitlab include Gitlab::Routing.url_helpers def self.match(text) - /\Adeploy\s+(?.*)\s+to+\s+(?.*)\z/.match(text) + /\Adeploy\s+(?\S+.*)\s+to+\s+(?\S+.*)\z/.match(text) end def self.help_message diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..e76733f5445 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/mattermost.rb b/lib/gitlab/chat_commands/presenters/mattermost.rb new file mode 100644 index 00000000000..67eda983a74 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/mattermost.rb @@ -0,0 +1,131 @@ +module Mattermost + class Presenter + class << self + include Gitlab::Routing.url_helpers + + def authorize_chat_name(url) + message = if url + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(message) + end + + def help(commands, trigger) + if commands.none? + ephemeral_response("No commands configured") + else + commands.map! { |command| "#{trigger} #{command}" } + message = header_with_list("Available commands", commands) + + ephemeral_response(message) + end + end + + def present(subject) + return not_found unless subject + + if subject.is_a?(Gitlab::ChatCommands::Result) + show_result(subject) + elsif subject.respond_to?(:count) + if subject.many? + multiple_resources(subject) + elsif subject.none? + not_found + else + single_resource(subject) + end + else + single_resource(subject) + end + end + + def access_denied + ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + + private + + def show_result(result) + case result.type + when :success + in_channel_response(result.message) + else + ephemeral_response(result.message) + end + end + + def not_found + ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") + end + + def single_resource(resource) + return error(resource) if resource.errors.any? || !resource.persisted? + + message = "### #{title(resource)}" + message << "\n\n#{resource.description}" if resource.try(:description) + + in_channel_response(message) + end + + def multiple_resources(resources) + resources.map! { |resource| title(resource) } + + message = header_with_list("Multiple results were found:", resources) + + ephemeral_response(message) + end + + def error(resource) + message = header_with_list("The action was not successful, because:", resource.errors.messages) + + ephemeral_response(message) + end + + def title(resource) + reference = resource.try(:to_reference) || resource.try(:id) + title = resource.try(:title) || resource.try(:name) + + "[#{reference} #{title}](#{url(resource)})" + end + + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + + def url(resource) + url_for( + [ + resource.project.namespace.becomes(Namespace), + resource.project, + resource + ] + ) + end + + def ephemeral_response(message) + { + response_type: :ephemeral, + text: message, + status: 200 + } + end + + def in_channel_response(message) + { + response_type: :in_channel, + text: message, + status: 200 + } + end + end + end +end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb deleted file mode 100644 index 67eda983a74..00000000000 --- a/lib/mattermost/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -module Mattermost - class Presenter - class << self - include Gitlab::Routing.url_helpers - - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end - - ephemeral_response(message) - end - - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) - - ephemeral_response(message) - end - end - - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.many? - multiple_resources(subject) - elsif subject.none? - not_found - else - single_resource(subject) - end - else - single_resource(subject) - end - end - - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - private - - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end - - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? - - message = "### #{title(resource)}" - message << "\n\n#{resource.description}" if resource.try(:description) - - in_channel_response(message) - end - - def multiple_resources(resources) - resources.map! { |resource| title(resource) } - - message = header_with_list("Multiple results were found:", resources) - - ephemeral_response(message) - end - - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) - - ephemeral_response(message) - end - - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) - - "[#{reference} #{title}](#{url(resource)})" - end - - def header_with_list(header, items) - message = [header] - - items.each do |item| - message << "- #{item}" - end - - message.join("\n") - end - - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end - - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end - - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } - end - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index bfc6818ac08..ed8d25a526a 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -64,7 +64,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -74,7 +74,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment from staging to production started') + expect(subject[:text]).to include('Deployment started from staging to production') expect(subject[:response_type]).to be(:in_channel) end @@ -91,4 +91,26 @@ describe Gitlab::ChatCommands::Command, service: true do end end end + + describe '#match_command' do + subject { described_class.new(project, user, params).match_command.first } + + context 'IssueShow is triggered' do + let(:params) { { text: 'issue show 123' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) } + end + + context 'IssueCreate is triggered' do + let(:params) { { text: 'issue create my title' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + end + + context 'IssueSearch is triggered' do + let(:params) { { text: 'issue search my query' } } + + it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) } + end + end end diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index b71d153f814..50ad5013df9 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'example.gitlab.com', + project_url: 'http://example.gitlab.com', commit: { status: status, @@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do end def build_message(status_text = status) - ":" \ - " Commit :" \ + " Commit " \ - " of branch" \ + " of branch" \ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb index ebe0ead4408..190ff4c535d 100644 --- a/spec/models/project_services/chat_message/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Issue title', id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', action: 'open', state: 'opened', description: 'issue description' @@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - '] Issue opened by test.user') + '[] Issue opened by test.user') expect(subject.attachments).to eq([ { title: "#100 Issue title", - title_link: "url", + title_link: "http://url.com", text: "issue description", color: color, } @@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - '] Issue closed by test.user') + '[] Issue closed by test.user') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index 07c414c6ca4..cc154112e90 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: "Issue title\nSecond line", id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', state: 'opened', description: 'issue description', source_branch: 'source_branch', @@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'test.user opened '\ - 'in : *Issue title*') + 'test.user opened '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end @@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'test.user closed '\ - 'in : *Issue title*') + 'test.user closed '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb index 31936da40a2..da700a08e57 100644 --- a/spec/models/project_services/chat_message/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do avatar_url: 'http://fakeavatar' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', repository: { name: 'project_name', - url: 'somewhere.com', + url: 'http://somewhere.com', }, object_attributes: { id: 10, note: 'comment on a commit', - url: 'url', + url: 'http://url.com', noteable_type: 'Commit' } } @@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on commits' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*Added a commit message*") expected_attachments = [ { @@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*merge request title*") expected_attachments = [ { @@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = described_class.new(@args) expect(message.pretext).to eq( - "test.user in : " \ + "test.user in : " \ "*issue title*") expected_attachments = [ { @@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a project snippet' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*snippet title*") expected_attachments = [ { diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index b781c4505db..24928873bad 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/heads/master', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end @@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do context 'push' do before do args[:commits] = [ - { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }, ] end it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch of '\ - ' ()' + 'test.user pushed to branch of '\ + ' ()' ) expect(subject.attachments).to eq([ { - text: ": message1 - author1\n"\ - ": message2 - author2", + text: ": message1 - author1\n"\ + ": message2 - author2", color: color, } ]) @@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/tags/new_tag', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - ' to ' \ - '') + ' to ' \ + '') expect(subject.attachments).to be_empty end end @@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch to '\ - '' + 'test.user pushed new branch to '\ + '' ) expect(subject.attachments).to be_empty end @@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch master from ' + 'test.user removed branch master from ' ) expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index 94c04dc0865..a2ad61e38e7 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Wiki page title', - url: 'url', + url: 'http://url.com', content: 'Wiki page description' } } @@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( - 'test.user created in : '\ + 'test.user created in : '\ '*Wiki page title*') end end @@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( - 'test.user edited in : '\ + 'test.user edited in : '\ '*Wiki page title*') end end -- cgit v1.2.1 From ed880e4954803f9753cfafe0dd8106852245d10d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Dec 2016 23:45:10 +0100 Subject: Rename ChatService into ChatSlashCommandsService --- app/models/project.rb | 1 - app/models/project_services/chat_service.rb | 21 --------- .../chat_slash_commands_service.rb | 55 ++++++++++++++++++++++ .../mattermost_slash_commands_service.rb | 28 +---------- .../slack_slash_commands_service.rb | 36 ++------------ 5 files changed, 60 insertions(+), 81 deletions(-) delete mode 100644 app/models/project_services/chat_service.rb create mode 100644 app/models/project_services/chat_slash_commands_service.rb diff --git a/app/models/project.rb b/app/models/project.rb index 7f3a4debfc6..9d8351399f9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -79,7 +79,6 @@ class Project < ActiveRecord::Base has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit, dependent: :destroy - has_many :chat_services # Project services has_one :campfire_service, dependent: :destroy diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb deleted file mode 100644 index 574788462de..00000000000 --- a/app/models/project_services/chat_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Base class for Chat services -# This class is not meant to be used directly, but only to inherit from. -class ChatService < Service - default_value_for :category, 'chat' - - has_many :chat_names, foreign_key: :service_id - - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) - end - - def supported_events - [] - end - - def trigger(params) - raise NotImplementedError - end -end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb new file mode 100644 index 00000000000..b21a1730285 --- /dev/null +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -0,0 +1,55 @@ +# Base class for Chat services +# This class is not meant to be used directly, but only to inherrit from. +class ChatSlashCommandsService < Service + default_value_for :category, 'chat' + + prop_accessor :token + + has_many :chat_names, foreign_key: :service_id + + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def supported_events + [] + end + + def can_test? + false + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' } + ] + end + + def trigger(params) + return nil unless valid_token?(params[:token]) + + user = find_chat_user(params) + unless user + url = authorize_chat_name_url(params) + return presenter.authorize_chat_name(url) + end + + Gitlab::ChatCommands::Command.new(presenter, project, user, params).execute + end + + private + + def find_chat_user(params) + ChatNames::FindUserService.new(self, params).execute + end + + def authorize_chat_name_url(params) + ChatNames::AuthorizeUserService.new(self, params).execute + end + + def presenter + throw NotImplementedError + 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..4c1e27cafbb 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -19,31 +19,7 @@ class MattermostSlashCommandsService < ChatService 'mattermost_slash_commands' end - def fields - [ - { type: 'text', name: 'token', placeholder: '' } - ] - end - - def trigger(params) - return nil unless valid_token?(params[:token]) - - user = find_chat_user(params) - unless user - url = authorize_chat_name_url(params) - return Mattermost::Presenter.authorize_chat_name(url) - end - - Gitlab::ChatCommands::Command.new(project, user, params).execute - end - - private - - def find_chat_user(params) - ChatNames::FindUserService.new(self, params).execute - end - - def authorize_chat_name_url(params) - ChatNames::AuthorizeUserService.new(self, params).execute + def presenter + Gitlab::ChatCommands::Presenters::Mattermost.new end end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 9c66f95eef9..978d48e8c18 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,12 +1,6 @@ -class SlackSlashCommandsService < ChatService +class SlackSlashCommandsService < ChatSlashCommandsService include TriggersHelper - prop_accessor :token - - def can_test? - false - end - def title 'Slack Slash Command' end @@ -19,31 +13,7 @@ class SlackSlashCommandsService < ChatService 'slack_slash_commands' end - def fields - [ - { type: 'text', name: 'token', placeholder: '' } - ] - end - - def trigger(params) - return nil unless valid_token?(params[:token]) - - user = find_chat_user(params) - unless user - url = authorize_chat_name_url(params) - return Gitlab::ChatCommands::Presenters::Access.new(url).authorize - end - - Gitlab::ChatCommands::Command.new(project, user, params).execute - end - - private - - def find_chat_user(params) - ChatNames::FindUserService.new(self, params).execute - end - - def authorize_chat_name_url(params) - ChatNames::AuthorizeUserService.new(self, params).execute + def presenter + Gitlab::ChatCommands::Presenters::Mattermost.new end end -- cgit v1.2.1 From 37057870a6b4bf4bf42cc7210a6ae17d68ae5448 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Dec 2016 23:45:46 +0100 Subject: Rename Mattermost::Presenter to Presenter --- lib/gitlab/chat_commands/presenter.rb | 133 ++++++++++++++++++++++ lib/gitlab/chat_commands/presenters/mattermost.rb | 131 --------------------- 2 files changed, 133 insertions(+), 131 deletions(-) create mode 100644 lib/gitlab/chat_commands/presenter.rb delete mode 100644 lib/gitlab/chat_commands/presenters/mattermost.rb diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb new file mode 100644 index 00000000000..3143dc092a1 --- /dev/null +++ b/lib/gitlab/chat_commands/presenter.rb @@ -0,0 +1,133 @@ +module Gitlab + class ChatCommands + class Presenter + class << self + include Gitlab::Routing.url_helpers + + def authorize_chat_name(url) + message = if url + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(message) + end + + def help(commands, trigger) + if commands.none? + ephemeral_response("No commands configured") + else + commands.map! { |command| "#{trigger} #{command}" } + message = header_with_list("Available commands", commands) + + ephemeral_response(message) + end + end + + def present(subject) + return not_found unless subject + + if subject.is_a?(Gitlab::ChatCommands::Result) + show_result(subject) + elsif subject.respond_to?(:count) + if subject.many? + multiple_resources(subject) + elsif subject.none? + not_found + else + single_resource(subject) + end + else + single_resource(subject) + end + end + + def access_denied + ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + + private + + def show_result(result) + case result.type + when :success + in_channel_response(result.message) + else + ephemeral_response(result.message) + end + end + + def not_found + ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") + end + + def single_resource(resource) + return error(resource) if resource.errors.any? || !resource.persisted? + + message = "### #{title(resource)}" + message << "\n\n#{resource.description}" if resource.try(:description) + + in_channel_response(message) + end + + def multiple_resources(resources) + resources.map! { |resource| title(resource) } + + message = header_with_list("Multiple results were found:", resources) + + ephemeral_response(message) + end + + def error(resource) + message = header_with_list("The action was not successful, because:", resource.errors.messages) + + ephemeral_response(message) + end + + def title(resource) + reference = resource.try(:to_reference) || resource.try(:id) + title = resource.try(:title) || resource.try(:name) + + "[#{reference} #{title}](#{url(resource)})" + end + + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + + def url(resource) + url_for( + [ + resource.project.namespace.becomes(Namespace), + resource.project, + resource + ] + ) + end + + def ephemeral_response(message) + { + response_type: :ephemeral, + text: message, + status: 200 + } + end + + def in_channel_response(message) + { + response_type: :in_channel, + text: message, + status: 200 + } + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/mattermost.rb b/lib/gitlab/chat_commands/presenters/mattermost.rb deleted file mode 100644 index 67eda983a74..00000000000 --- a/lib/gitlab/chat_commands/presenters/mattermost.rb +++ /dev/null @@ -1,131 +0,0 @@ -module Mattermost - class Presenter - class << self - include Gitlab::Routing.url_helpers - - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end - - ephemeral_response(message) - end - - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) - - ephemeral_response(message) - end - end - - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.many? - multiple_resources(subject) - elsif subject.none? - not_found - else - single_resource(subject) - end - else - single_resource(subject) - end - end - - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - private - - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end - - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? - - message = "### #{title(resource)}" - message << "\n\n#{resource.description}" if resource.try(:description) - - in_channel_response(message) - end - - def multiple_resources(resources) - resources.map! { |resource| title(resource) } - - message = header_with_list("Multiple results were found:", resources) - - ephemeral_response(message) - end - - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) - - ephemeral_response(message) - end - - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) - - "[#{reference} #{title}](#{url(resource)})" - end - - def header_with_list(header, items) - message = [header] - - items.each do |item| - message << "- #{item}" - end - - message.join("\n") - end - - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end - - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end - - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } - end - end - end -end -- cgit v1.2.1 From 8ca5fef9d472e11b4a9ff5d5ab47bbba3e7e670d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Dec 2016 23:48:30 +0100 Subject: Use single presenter for everything --- app/models/project_services/chat_slash_commands_service.rb | 8 ++------ app/models/project_services/mattermost_slash_commands_service.rb | 4 ---- app/models/project_services/slack_slash_commands_service.rb | 4 ---- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index b21a1730285..7ff80447a1c 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -33,10 +33,10 @@ class ChatSlashCommandsService < Service user = find_chat_user(params) unless user url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + return Gitlab::ChatCommands::Presenter.authorize_chat_name(url) end - Gitlab::ChatCommands::Command.new(presenter, project, user, params).execute + Gitlab::ChatCommands::Command.new(project, user, params).execute end private @@ -48,8 +48,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - throw NotImplementedError - 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 4c1e27cafbb..6aac7c2788b 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -18,8 +18,4 @@ class MattermostSlashCommandsService < ChatService def to_param 'mattermost_slash_commands' end - - def presenter - Gitlab::ChatCommands::Presenters::Mattermost.new - end end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 978d48e8c18..197d8eb7bca 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -12,8 +12,4 @@ class SlackSlashCommandsService < ChatSlashCommandsService def to_param 'slack_slash_commands' end - - def presenter - Gitlab::ChatCommands::Presenters::Mattermost.new - end end -- cgit v1.2.1 From dc995daf90e67b5531dd7fbb8a958507f46e4eb6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 15 Dec 2016 23:51:57 +0100 Subject: Use Slack compatible syntax --- lib/gitlab/chat_commands/presenter.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index 3143dc092a1..14c78ac39aa 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -6,7 +6,7 @@ module Gitlab def authorize_chat_name(url) message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." + ":wave: Hi there! Before I do anything for you, please <#{url}|connect your GitLab account>." else ":sweat_smile: Couldn't identify you, nor can I autorize you!" end @@ -44,7 +44,7 @@ module Gitlab end def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + ephemeral_response("Whoops! That action is not allowed. This incident will be .") end private @@ -65,7 +65,7 @@ module Gitlab def single_resource(resource) return error(resource) if resource.errors.any? || !resource.persisted? - message = "### #{title(resource)}" + message = "#{title(resource)}:" message << "\n\n#{resource.description}" if resource.try(:description) in_channel_response(message) @@ -89,7 +89,7 @@ module Gitlab reference = resource.try(:to_reference) || resource.try(:id) title = resource.try(:title) || resource.try(:name) - "[#{reference} #{title}](#{url(resource)})" + "<#{url(resource)}|#{reference} #{title}>" end def header_with_list(header, items) -- cgit v1.2.1 From cc83aded33471a3e2f943bd9a760f689d30f901e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 16 Dec 2016 00:00:54 +0100 Subject: Render format dependent links --- .../chat_slash_commands_service.rb | 13 +- .../mattermost_slash_commands_service.rb | 4 + .../slack_slash_commands_service.rb | 4 + lib/gitlab/chat_commands/base_command.rb | 4 + lib/gitlab/chat_commands/command.rb | 6 +- lib/gitlab/chat_commands/presenter.rb | 200 +++++++++++---------- 6 files changed, 132 insertions(+), 99 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 7ff80447a1c..f11d257e6c1 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -33,10 +33,11 @@ class ChatSlashCommandsService < Service user = find_chat_user(params) unless user url = authorize_chat_name_url(params) - return Gitlab::ChatCommands::Presenter.authorize_chat_name(url) + return presenter.authorize_chat_name(url) end - Gitlab::ChatCommands::Command.new(project, user, params).execute + Gitlab::ChatCommands::Command.new(project, user, + params.merge(presenter_format: presenter_format)).execute end private @@ -48,4 +49,12 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end + + def presenter + Gitlab::ChatCommands::Presenter.new(presenter_format) + end + + def presenter_format + throw NotImplementedError + 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 6aac7c2788b..f9d4b29f4ea 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -18,4 +18,8 @@ class MattermostSlashCommandsService < ChatService def to_param 'mattermost_slash_commands' end + + def presenter_format + 'mattermost' + end end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 197d8eb7bca..6bf10ff6572 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -12,4 +12,8 @@ class SlackSlashCommandsService < ChatSlashCommandsService def to_param 'slack_slash_commands' end + + def presenter_format + 'slack' + end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 25da8474e95..156bb826f86 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,6 +42,10 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end + + def presenter + Gitlab::ChatCommands::Presenter.new(params[:presenter_format]) + end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index b0d3fdbc48a..c5c54cf7cfc 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -48,15 +48,15 @@ module Gitlab end def help(messages) - Mattermost::Presenter.help(messages, params[:command]) + presenter.help(messages, params[:command]) end def access_denied - Mattermost::Presenter.access_denied + presenter.access_denied end def present(resource) - Mattermost::Presenter.present(resource) + presenter.present(resource) end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index 14c78ac39aa..e151513cbd5 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -1,131 +1,143 @@ module Gitlab class ChatCommands class Presenter - class << self - include Gitlab::Routing.url_helpers + include Gitlab::Routing.url_helpers - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please <#{url}|connect your GitLab account>." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end + attr_reader :format - ephemeral_response(message) - end + def initialize(format) + @format = format + end - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) + def authorize_chat_name(url) + message = if url + ":wave: Hi there! Before I do anything for you, please #{link(url, 'connect your GitLab account')}." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end - ephemeral_response(message) - end + ephemeral_response(message) + end + + def help(commands, trigger) + if commands.none? + ephemeral_response("No commands configured") + else + commands.map! { |command| "#{trigger} #{command}" } + message = header_with_list("Available commands", commands) + + ephemeral_response(message) end + end - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.many? - multiple_resources(subject) - elsif subject.none? - not_found - else - single_resource(subject) - end + def present(subject) + return not_found unless subject + + if subject.is_a?(Gitlab::ChatCommands::Result) + show_result(subject) + elsif subject.respond_to?(:count) + if subject.many? + multiple_resources(subject) + elsif subject.none? + not_found else single_resource(subject) end + else + single_resource(subject) end + end - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be .") - end + def access_denied + ephemeral_response("Whoops! That action is not allowed. This incident will be #{link('https://xkcd.com/838/', 'reported')}.") + end - private + private - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end + def show_result(result) + case result.type + when :success + in_channel_response(result.message) + else + ephemeral_response(result.message) end + end - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end + def not_found + ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") + end - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? + def single_resource(resource) + return error(resource) if resource.errors.any? || !resource.persisted? - message = "#{title(resource)}:" - message << "\n\n#{resource.description}" if resource.try(:description) + message = "#{title(resource)}:" + message << "\n\n#{resource.description}" if resource.try(:description) - in_channel_response(message) - end + in_channel_response(message) + end - def multiple_resources(resources) - resources.map! { |resource| title(resource) } + def multiple_resources(resources) + resources.map! { |resource| title(resource) } - message = header_with_list("Multiple results were found:", resources) + message = header_with_list("Multiple results were found:", resources) - ephemeral_response(message) - end + ephemeral_response(message) + end - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) + def error(resource) + message = header_with_list("The action was not successful, because:", resource.errors.messages) - ephemeral_response(message) - end + ephemeral_response(message) + end - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) + def title(resource) + reference = resource.try(:to_reference) || resource.try(:id) + title = resource.try(:title) || resource.try(:name) - "<#{url(resource)}|#{reference} #{title}>" - end + link(url(resource), "#{reference} #{title}") + end - def header_with_list(header, items) - message = [header] + def header_with_list(header, items) + message = [header] - items.each do |item| - message << "- #{item}" - end - - message.join("\n") + items.each do |item| + message << "- #{item}" end - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end + message.join("\n") + end - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end + def url(resource) + url_for( + [ + resource.project.namespace.becomes(Namespace), + resource.project, + resource + ] + ) + end + + def ephemeral_response(message) + { + response_type: :ephemeral, + text: message, + status: 200 + } + end + + def in_channel_response(message) + { + response_type: :in_channel, + text: message, + status: 200 + } + end - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } + def link(url, title) + case format + when 'slack' then "<#{url}|#{title}>" + when 'mattermost' then "[#{title}](#{url})" + else then title end end end -- cgit v1.2.1 From ebc3f62be52e386b4550e01ce589744e880ad443 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 16 Dec 2016 13:32:05 +0100 Subject: Fix specs --- .../services/mattermost_slash_commands/_help.html.haml | 5 ++--- lib/gitlab/chat_commands/command.rb | 4 ++-- lib/gitlab/chat_commands/presenter.rb | 11 ++++++++--- 3 files changed, 12 insertions(+), 8 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 2bdd4cd148c..01a77a952d1 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,5 +1,4 @@ -- pretty_path_with_namespace = "#{@project.namespace ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}" -- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}" +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" .well This service allows GitLab users to perform common operations on this @@ -27,7 +26,7 @@ .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' + = 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') diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index c5c54cf7cfc..145086755e4 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -22,8 +22,6 @@ module Gitlab end end - private - def match_command match = nil service = available_commands.find do |klass| @@ -33,6 +31,8 @@ module Gitlab [service, match] end + private + def help_messages available_commands.map(&:help_message) end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index e151513cbd5..c86efc0c3f8 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -135,9 +135,14 @@ module Gitlab def link(url, title) case format - when 'slack' then "<#{url}|#{title}>" - when 'mattermost' then "[#{title}](#{url})" - else then title + when 'slack' + "<#{url}|#{title}>" + + when 'mattermost' + "[#{title}](#{url})" + + else + title end end end -- cgit v1.2.1 From f9f1a508c6a4bdb2fcee98d18394e28e1cc663c3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 16 Dec 2016 14:21:06 +0100 Subject: Fix SlackSlashCommands tests --- .../chat_slash_commands_service.rb | 2 +- .../mattermost_slash_commands_service.rb | 2 +- lib/gitlab/chat_commands/presenter.rb | 2 +- spec/lib/gitlab/chat_commands/command_spec.rb | 20 +++- spec/models/project_services/chat_service_spec.rb | 15 --- .../chat_slash_commands_service_spec.rb | 103 +++++++++++++++++++++ .../mattermost_slash_commands_service_spec.rb | 96 +------------------ .../slack_slash_commands_service.rb | 5 + 8 files changed, 127 insertions(+), 118 deletions(-) delete mode 100644 spec/models/project_services/chat_service_spec.rb create mode 100644 spec/models/project_services/chat_slash_commands_service_spec.rb create mode 100644 spec/models/project_services/slack_slash_commands_service.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index f11d257e6c1..e58c96f5094 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -5,7 +5,7 @@ class ChatSlashCommandsService < Service prop_accessor :token - has_many :chat_names, foreign_key: :service_id + has_many :chat_names, foreign_key: :service_id, dependent: :destroy def valid_token?(token) self.respond_to?(:token) && diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index f9d4b29f4ea..72a7b9c8f3a 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,4 +1,4 @@ -class MattermostSlashCommandsService < ChatService +class MattermostSlashCommandsService < ChatSlashCommandsService include TriggersHelper prop_accessor :token diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index c86efc0c3f8..98356ebebb3 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -1,5 +1,5 @@ module Gitlab - class ChatCommands + module ChatCommands class Presenter include Gitlab::Routing.url_helpers diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index ed8d25a526a..dec98d990b2 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -3,9 +3,13 @@ require 'spec_helper' describe Gitlab::ChatCommands::Command, service: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:format) { nil } describe '#execute' do - subject { described_class.new(project, user, params).execute } + subject do + described_class.new(project, user, + params.merge(presenter_format: format)).execute + end context 'when no command is available' do let(:params) { { text: 'issue show 1' } } @@ -47,8 +51,14 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to match("my new issue") end - it 'shows a link to the new issue' do - expect(subject[:text]).to match(/\/issues\/\d+/) + %w(slack mattermost).each do |format| + context "for #{format}" do + let(:format) { format } + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(/\/issues\/\d+/) + end + end end end @@ -64,7 +74,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! This action is not allowed') + expect(subject[:text]).to start_with('Whoops! That action is not allowed') end end @@ -74,7 +84,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment started from staging to production') + expect(subject[:text]).to include('Deployment from staging to production started.') expect(subject[:response_type]).to be(:in_channel) end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb deleted file mode 100644 index c6a45a3e1be..00000000000 --- a/spec/models/project_services/chat_service_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe ChatService, models: true do - describe "Associations" do - it { is_expected.to have_many :chat_names } - end - - describe '#valid_token?' do - subject { described_class.new } - - it 'is false as it has no token' do - expect(subject.valid_token?('wer')).to be_falsey - end - end -end diff --git a/spec/models/project_services/chat_slash_commands_service_spec.rb b/spec/models/project_services/chat_slash_commands_service_spec.rb new file mode 100644 index 00000000000..64fdd4d570b --- /dev/null +++ b/spec/models/project_services/chat_slash_commands_service_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe ChatSlashCommandsService, models: true do + describe "Associations" do + it { is_expected.to respond_to :token } + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + context 'when the token is empty' do + it 'is false' do + expect(subject.valid_token?('wer')).to be_falsey + end + end + + context 'when there is a token' do + before do + subject.token = '123' + end + + it 'accepts equal tokens' do + expect(subject.valid_token?('123')).to be_truthy + end + end + end + + describe '#trigger' do + subject { described_class.new } + + before do + allow(subject).to receive(:presenter_format).and_return('unknown') + end + + context 'no token is passed' do + let(:params) { Hash.new } + + it 'returns nil' do + expect(subject.trigger(params)).to be_nil + end + end + + context 'with a token passed' do + let(:project) { create(:empty_project) } + let(:params) { { token: 'token' } } + + before do + allow(subject).to receive(:token).and_return('token') + end + + context 'no user can be found' do + context 'when no url can be generated' do + it 'responds with the authorize url' do + response = subject.trigger(params) + + expect(response[:response_type]).to eq :ephemeral + expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" + end + end + + context 'when an auth url can be generated' do + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_mattermost_slash_commands_service( + properties: { token: 'token' } + ) + end + + it 'generates the url' do + response = service.trigger(params) + + expect(response[:text]).to start_with(':wave: Hi there!') + end + end + end + + context 'when the user is authenticated' do + let!(:chat_name) { create(:chat_name, service: subject) } + let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + subject do + described_class.create(project: project, properties: { token: 'token' }) + end + + it 'triggers the command' do + expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + + subject.trigger(params) + end + 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 4a1037e950b..b9deb0201e1 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -1,99 +1,5 @@ require 'spec_helper' describe MattermostSlashCommandsService, models: true do - describe "Associations" do - it { is_expected.to respond_to :token } - end - - describe '#valid_token?' do - subject { described_class.new } - - context 'when the token is empty' do - it 'is false' do - expect(subject.valid_token?('wer')).to be_falsey - end - end - - context 'when there is a token' do - before do - subject.token = '123' - end - - it 'accepts equal tokens' do - expect(subject.valid_token?('123')).to be_truthy - end - end - end - - describe '#trigger' do - subject { described_class.new } - - context 'no token is passed' do - let(:params) { Hash.new } - - it 'returns nil' do - expect(subject.trigger(params)).to be_nil - end - end - - context 'with a token passed' do - let(:project) { create(:empty_project) } - let(:params) { { token: 'token' } } - - before do - allow(subject).to receive(:token).and_return('token') - end - - context 'no user can be found' do - context 'when no url can be generated' do - it 'responds with the authorize url' do - response = subject.trigger(params) - - expect(response[:response_type]).to eq :ephemeral - expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" - end - end - - context 'when an auth url can be generated' do - let(:params) do - { - team_domain: 'http://domain.tld', - team_id: 'T3423423', - user_id: 'U234234', - user_name: 'mepmep', - token: 'token' - } - end - - let(:service) do - project.create_mattermost_slash_commands_service( - properties: { token: 'token' } - ) - end - - it 'generates the url' do - response = service.trigger(params) - - expect(response[:text]).to start_with(':wave: Hi there!') - end - end - end - - context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, service: service) } - let(:service) do - project.create_mattermost_slash_commands_service( - properties: { token: 'token' } - ) - end - let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } - - it 'triggers the command' do - expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) - - service.trigger(params) - end - end - end - end + it { is_expected.to respond_to :presenter_format } end diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service.rb new file mode 100644 index 00000000000..5ef97b9a2ed --- /dev/null +++ b/spec/models/project_services/slack_slash_commands_service.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe SlackSlashCommandsService, models: true do + it { is_expected.to respond_to :presenter_format } +end -- cgit v1.2.1 From 0f2776287a7d9b0fde9ff54ef8d9f74e2f844a09 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 16 Dec 2016 15:08:10 +0100 Subject: Use Slack::Notifier::LinkFormatter to convert markdown links to slack compat --- .../chat_slash_commands_service.rb | 8 +- .../mattermost_slash_commands_service.rb | 4 - .../slack_slash_commands_service.rb | 11 +- lib/gitlab/chat_commands/base_command.rb | 2 +- lib/gitlab/chat_commands/presenter.rb | 25 +- spec/lib/gitlab/chat_commands/command_spec.rb | 14 +- .../chat_slash_commands_service_spec.rb | 103 ------- .../mattermost_notification_service_spec.rb | 2 +- .../mattermost_slash_commands_service_spec.rb | 2 +- .../slack_notification_service_spec.rb | 2 +- .../slack_slash_commands_service.rb | 35 ++- .../support/chat_slash_commands_shared_examples.rb | 97 ++++++ ...ack_mattermost_notifications_shared_examples.rb | 328 +++++++++++++++++++++ spec/support/slack_mattermost_shared_examples.rb | 328 --------------------- 14 files changed, 480 insertions(+), 481 deletions(-) delete mode 100644 spec/models/project_services/chat_slash_commands_service_spec.rb create mode 100644 spec/support/chat_slash_commands_shared_examples.rb create mode 100644 spec/support/slack_mattermost_notifications_shared_examples.rb delete mode 100644 spec/support/slack_mattermost_shared_examples.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index e58c96f5094..12261e9821e 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -37,7 +37,7 @@ class ChatSlashCommandsService < Service end Gitlab::ChatCommands::Command.new(project, user, - params.merge(presenter_format: presenter_format)).execute + params).execute end private @@ -51,10 +51,6 @@ class ChatSlashCommandsService < Service end def presenter - Gitlab::ChatCommands::Presenter.new(presenter_format) - end - - def presenter_format - throw NotImplementedError + Gitlab::ChatCommands::Presenter.new 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 72a7b9c8f3a..10740275669 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -18,8 +18,4 @@ class MattermostSlashCommandsService < ChatSlashCommandsService def to_param 'mattermost_slash_commands' end - - def presenter_format - 'mattermost' - end end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 6bf10ff6572..8413c657099 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -13,7 +13,14 @@ class SlackSlashCommandsService < ChatSlashCommandsService 'slack_slash_commands' end - def presenter_format - 'slack' + def trigger(params) + result = super + + # Format messages to be Slack-compatible + if result && result[:text] + result[:text] = Slack::Notifier::LinkFormatter.format(result[:text]) + end + + result end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 156bb826f86..4fe53ce93a9 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -44,7 +44,7 @@ module Gitlab end def presenter - Gitlab::ChatCommands::Presenter.new(params[:presenter_format]) + Gitlab::ChatCommands::Presenter.new end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index 98356ebebb3..e94d0ce2470 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -3,15 +3,9 @@ module Gitlab class Presenter include Gitlab::Routing.url_helpers - attr_reader :format - - def initialize(format) - @format = format - end - def authorize_chat_name(url) message = if url - ":wave: Hi there! Before I do anything for you, please #{link(url, 'connect your GitLab account')}." + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." else ":sweat_smile: Couldn't identify you, nor can I autorize you!" end @@ -49,7 +43,7 @@ module Gitlab end def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be #{link('https://xkcd.com/838/', 'reported')}.") + ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") end private @@ -94,7 +88,7 @@ module Gitlab reference = resource.try(:to_reference) || resource.try(:id) title = resource.try(:title) || resource.try(:name) - link(url(resource), "#{reference} #{title}") + "[#{reference} #{title}](#{url(resource)})" end def header_with_list(header, items) @@ -132,19 +126,6 @@ module Gitlab status: 200 } end - - def link(url, title) - case format - when 'slack' - "<#{url}|#{title}>" - - when 'mattermost' - "[#{title}](#{url})" - - else - title - end - end end end end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index dec98d990b2..a0ec8884635 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -3,12 +3,10 @@ require 'spec_helper' describe Gitlab::ChatCommands::Command, service: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:format) { nil } describe '#execute' do subject do - described_class.new(project, user, - params.merge(presenter_format: format)).execute + described_class.new(project, user, params).execute end context 'when no command is available' do @@ -51,14 +49,8 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to match("my new issue") end - %w(slack mattermost).each do |format| - context "for #{format}" do - let(:format) { format } - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(/\/issues\/\d+/) - end - end + it 'shows a link to the new issue' do + expect(subject[:text]).to match(/\/issues\/\d+/) end end diff --git a/spec/models/project_services/chat_slash_commands_service_spec.rb b/spec/models/project_services/chat_slash_commands_service_spec.rb deleted file mode 100644 index 64fdd4d570b..00000000000 --- a/spec/models/project_services/chat_slash_commands_service_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -describe ChatSlashCommandsService, models: true do - describe "Associations" do - it { is_expected.to respond_to :token } - it { is_expected.to have_many :chat_names } - end - - describe '#valid_token?' do - subject { described_class.new } - - context 'when the token is empty' do - it 'is false' do - expect(subject.valid_token?('wer')).to be_falsey - end - end - - context 'when there is a token' do - before do - subject.token = '123' - end - - it 'accepts equal tokens' do - expect(subject.valid_token?('123')).to be_truthy - end - end - end - - describe '#trigger' do - subject { described_class.new } - - before do - allow(subject).to receive(:presenter_format).and_return('unknown') - end - - context 'no token is passed' do - let(:params) { Hash.new } - - it 'returns nil' do - expect(subject.trigger(params)).to be_nil - end - end - - context 'with a token passed' do - let(:project) { create(:empty_project) } - let(:params) { { token: 'token' } } - - before do - allow(subject).to receive(:token).and_return('token') - end - - context 'no user can be found' do - context 'when no url can be generated' do - it 'responds with the authorize url' do - response = subject.trigger(params) - - expect(response[:response_type]).to eq :ephemeral - expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" - end - end - - context 'when an auth url can be generated' do - let(:params) do - { - team_domain: 'http://domain.tld', - team_id: 'T3423423', - user_id: 'U234234', - user_name: 'mepmep', - token: 'token' - } - end - - let(:service) do - project.create_mattermost_slash_commands_service( - properties: { token: 'token' } - ) - end - - it 'generates the url' do - response = service.trigger(params) - - expect(response[:text]).to start_with(':wave: Hi there!') - end - end - end - - context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, service: subject) } - let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } - - subject do - described_class.create(project: project, properties: { token: 'token' }) - end - - it 'triggers the command' do - expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) - - subject.trigger(params) - end - end - end - end -end diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb index c01e64b4c8e..7832d6f50cf 100644 --- a/spec/models/project_services/mattermost_notification_service_spec.rb +++ b/spec/models/project_services/mattermost_notification_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' describe MattermostNotificationService, models: true do - it_behaves_like "slack or mattermost" + it_behaves_like "slack or mattermost notifications" 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 b9deb0201e1..5c34cb6b4cf 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' describe MattermostSlashCommandsService, models: true do - it { is_expected.to respond_to :presenter_format } + it_behaves_like "chat slash commands" end diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb index 59ddddf7454..110b5bf2115 100644 --- a/spec/models/project_services/slack_notification_service_spec.rb +++ b/spec/models/project_services/slack_notification_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' describe SlackNotificationService, models: true do - it_behaves_like "slack or mattermost" + it_behaves_like "slack or mattermost notifications" end diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service.rb index 5ef97b9a2ed..c3fa80caebe 100644 --- a/spec/models/project_services/slack_slash_commands_service.rb +++ b/spec/models/project_services/slack_slash_commands_service.rb @@ -1,5 +1,38 @@ require 'spec_helper' describe SlackSlashCommandsService, models: true do - it { is_expected.to respond_to :presenter_format } + it_behaves_like "chat slash commands" + + describe '#trigger' do + context 'when an auth url is generated' do + let(:project) { create(:empty_project) } + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + let(:service) do + project.create_slack_slash_commands_service( + properties: { token: 'token' } + ) + end + let(:authorize_url) do + 'http://authorize.example.com/' + end + + before do + allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url) + end + + it 'uses slack compatible links' do + response = service.trigger(params) + + expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>") + end + end + end end diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb new file mode 100644 index 00000000000..96130b45235 --- /dev/null +++ b/spec/support/chat_slash_commands_shared_examples.rb @@ -0,0 +1,97 @@ +RSpec.shared_examples 'chat slash commands' do + describe "Associations" do + it { is_expected.to respond_to :token } + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + context 'when the token is empty' do + it 'is false' do + expect(subject.valid_token?('wer')).to be_falsey + end + end + + context 'when there is a token' do + before do + subject.token = '123' + end + + it 'accepts equal tokens' do + expect(subject.valid_token?('123')).to be_truthy + end + end + end + + describe '#trigger' do + subject { described_class.new } + + context 'no token is passed' do + let(:params) { Hash.new } + + it 'returns nil' do + expect(subject.trigger(params)).to be_nil + end + end + + context 'with a token passed' do + let(:project) { create(:empty_project) } + let(:params) { { token: 'token' } } + + before do + allow(subject).to receive(:token).and_return('token') + end + + context 'no user can be found' do + context 'when no url can be generated' do + it 'responds with the authorize url' do + response = subject.trigger(params) + + expect(response[:response_type]).to eq :ephemeral + expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" + end + end + + context 'when an auth url can be generated' do + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_mattermost_slash_commands_service( + properties: { token: 'token' } + ) + end + + it 'generates the url' do + response = service.trigger(params) + + expect(response[:text]).to start_with(':wave: Hi there!') + end + end + end + + context 'when the user is authenticated' do + let!(:chat_name) { create(:chat_name, service: subject) } + let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + subject do + described_class.create(project: project, properties: { token: 'token' }) + end + + it 'triggers the command' do + expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + + subject.trigger(params) + end + end + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb new file mode 100644 index 00000000000..8582aea5fe5 --- /dev/null +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -0,0 +1,328 @@ +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } + +RSpec.shared_examples 'slack or mattermost notifications' do + let(:chat_service) { described_class.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like 'issue tracker service URL attribute', :webhook + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:webhook) } + end + end + + describe "#execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') + + opts = { + title: "Awesome wiki_page", + content: "Some text describing some thing or another", + format: "md", + message: "user created page: Awesome wiki_page" + } + + wiki_page_service = WikiPages::CreateService.new(project, user, opts) + @wiki_page = wiki_page_service.execute + @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') + end + + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'uses the username as an option for slack when configured' 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). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it 'uses the channel as an option when it is configured' do + allow(chat_service).to receive(:channel).and_return(channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + chat_service.execute(push_sample_data) + end + + context "event channels" do + it "uses the right channel for push event" do + chat_service.update_attributes(push_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it "uses the right channel for merge request event" do + chat_service.update_attributes(merge_request_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@merge_sample_data) + end + + it "uses the right channel for issue event" do + chat_service.update_attributes(issue_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@issues_sample_data) + end + + it "uses the right channel for wiki event" do + chat_service.update_attributes(wiki_page_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@wiki_page_sample_data) + end + + context "note event" do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "uses the right channel" do + chat_service.update_attributes(note_channel: "random") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(note_data) + end + end + end + end + + describe "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + + before do + allow(chat_service).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'when commit comment event executed' do + let(:commit_note) do + create(:note_on_commit, author: user, + project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end + + it "calls Slack/Mattermost API for commit comment events" do + data = Gitlab::DataBuilder::Note.build(commit_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when merge request comment event executed' do + let(:merge_request_note) do + create(:note_on_merge_request, project: project, + note: "merge request note") + end + + it "calls Slack API for merge request comment events" do + data = Gitlab::DataBuilder::Note.build(merge_request_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when issue comment event executed' do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "calls Slack API for issue comment events" do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'when snippet comment event executed' do + let(:snippet_note) do + create(:note_on_project_snippet, project: project, + note: "snippet note") + end + + it "calls Slack API for snippet comment events" do + data = Gitlab::DataBuilder::Note.build(snippet_note, user) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(chat_service).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack/Mattermost API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + chat_service.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack/Mattermost API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack/Mattermost API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = chat_service.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + chat_service.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack/Mattermost API' + end + end + end +end diff --git a/spec/support/slack_mattermost_shared_examples.rb b/spec/support/slack_mattermost_shared_examples.rb deleted file mode 100644 index 56d4965f74d..00000000000 --- a/spec/support/slack_mattermost_shared_examples.rb +++ /dev/null @@ -1,328 +0,0 @@ -Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } - -RSpec.shared_examples 'slack or mattermost' do - let(:chat_service) { described_class.new } - let(:webhook_url) { 'https://example.gitlab.com/' } - - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before { subject.active = true } - - it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like 'issue tracker service URL attribute', :webhook - end - - context 'when service is inactive' do - before { subject.active = false } - - it { is_expected.not_to validate_presence_of(:webhook) } - end - end - - describe "#execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } - - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(chat_service).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - - opts = { - title: 'Awesome issue', - description: 'please fix' - } - - issue_service = Issues::CreateService.new(project, user, opts) - @issue = issue_service.execute - @issues_sample_data = issue_service.hook_data(@issue, 'open') - - opts = { - title: 'Awesome merge_request', - description: 'please fix', - source_branch: 'feature', - target_branch: 'master' - } - merge_service = MergeRequests::CreateService.new(project, - user, opts) - @merge_request = merge_service.execute - @merge_sample_data = merge_service.hook_data(@merge_request, - 'open') - - opts = { - title: "Awesome wiki_page", - content: "Some text describing some thing or another", - format: "md", - message: "user created page: Awesome wiki_page" - } - - wiki_page_service = WikiPages::CreateService.new(project, user, opts) - @wiki_page = wiki_page_service.execute - @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') - end - - it "calls Slack/Mattermost API for push events" do - chat_service.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack/Mattermost API for issue events" do - chat_service.execute(@issues_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack/Mattermost API for merge requests events" do - chat_service.execute(@merge_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack/Mattermost API for wiki page events" do - chat_service.execute(@wiki_page_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it 'uses the username as an option for slack when configured' 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). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(push_sample_data) - end - - it 'uses the channel as an option when it is configured' do - allow(chat_service).to receive(:channel).and_return(channel) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: channel). - and_return( - double(:slack_service).as_null_object - ) - chat_service.execute(push_sample_data) - end - - context "event channels" do - it "uses the right channel for push event" do - chat_service.update_attributes(push_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(push_sample_data) - end - - it "uses the right channel for merge request event" do - chat_service.update_attributes(merge_request_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(@merge_sample_data) - end - - it "uses the right channel for issue event" do - chat_service.update_attributes(issue_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(@issues_sample_data) - end - - it "uses the right channel for wiki event" do - chat_service.update_attributes(wiki_page_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(@wiki_page_sample_data) - end - - context "note event" do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "uses the right channel" do - chat_service.update_attributes(note_channel: "random") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - chat_service.execute(note_data) - end - end - end - end - - describe "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - - before do - allow(chat_service).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - end - - context 'when commit comment event executed' do - let(:commit_note) do - create(:note_on_commit, author: user, - project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') - end - - it "calls Slack/Mattermost API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when merge request comment event executed' do - let(:merge_request_note) do - create(:note_on_merge_request, project: project, - note: "merge request note") - end - - it "calls Slack API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when issue comment event executed' do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "calls Slack API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'when snippet comment event executed' do - let(:snippet_note) do - create(:note_on_project_snippet, project: project, - note: "snippet note") - end - - it "calls Slack API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - end - - describe 'Pipeline events' do - let(:user) { create(:user) } - let(:project) { create(:project) } - - let(:pipeline) do - create(:ci_pipeline, - project: project, status: status, - sha: project.commit.sha, ref: project.default_branch) - end - - before do - allow(chat_service).to receive_messages( - project: project, - service_hook: true, - webhook: webhook_url - ) - end - - shared_examples 'call Slack/Mattermost API' do - before do - WebMock.stub_request(:post, webhook_url) - end - - it 'calls Slack/Mattermost API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - chat_service.execute(data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - - it_behaves_like 'call Slack/Mattermost API' - end - - context 'with succeeded pipeline' do - let(:status) { 'success' } - - context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack/Mattermost API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = chat_service.execute(data) - - expect(result).to be_falsy - end - end - - context 'with setting notify_only_broken_pipelines to false' do - before do - chat_service.notify_only_broken_pipelines = false - end - - it_behaves_like 'call Slack/Mattermost API' - end - end - end -end -- cgit v1.2.1 From c46b758b48400efff5241efafad94c2ddbb8224c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Dec 2016 17:56:27 +0000 Subject: Remove unused style for dropdown. Improves animation --- app/assets/stylesheets/pages/pipelines.scss | 48 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a0719b415e5..b3deac3ab75 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -466,6 +466,28 @@ margin-bottom: 10px; white-space: normal; + + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-light; + flex-grow: 1; + + + &:focus { + outline: none; + } + + &:hover { + color: $gl-text-color; + + .dropdown-counter-badge { + color: $gl-text-color; + } + } + } + &:hover { background-color: $stage-hover-bg; border: 1px solid $stage-hover-border; @@ -635,28 +657,6 @@ font-weight: 100; } - -.dropdown-menu-toggle { - background-color: transparent; - border: none; - padding: 0; - color: $gl-text-color-light; - flex-grow: 1; - - - &:focus { - outline: none; - } - - &:hover { - color: $gl-text-color; - - .dropdown-counter-badge { - color: $gl-text-color; - } - } -} - // Action Icons .ci-action-icon-container .ci-action-icon-wrapper { float: right; @@ -816,7 +816,7 @@ .ci-status-icon { width: 28px; padding: 0 8px 0 0; - transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); + transition: width 0.2s cubic-bezier(0.25, 0, 1, 1); + .dropdown-caret { display: inline-block; @@ -853,7 +853,6 @@ .ci-status-icon-skipped { background-color: rgba($gray-darkest, .1); } - } .mini-pipeline-graph-icon-container { @@ -861,7 +860,6 @@ .ci-status-icon:focus { width: 28px; padding: 0 8px 0 0; - transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); + .dropdown-caret { display: inline-block; -- cgit v1.2.1 From 1aa3294cd2791ec0386059256560d22f7650620b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Dec 2016 18:07:10 +0000 Subject: Revert unwanted changes to the db/schema.rb file --- db/schema.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 78e3a356f74..3786a41f9a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -527,7 +527,6 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.string "type" t.string "fingerprint" t.boolean "public", default: false, null: false - t.boolean "can_push", default: false, null: false end add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree @@ -740,8 +739,8 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.text "description_html" t.boolean "lfs_enabled" + t.text "description_html" t.integer "parent_id" end @@ -1222,8 +1221,8 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "incoming_email_token" t.string "organization" + t.string "incoming_email_token" t.boolean "authorized_projects_populated" end -- cgit v1.2.1 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: Incorporate review --- app/controllers/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 -- cgit v1.2.1 From 178f9ebb67bddbd2719cbd140967103b57f2c0c7 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 15 Dec 2016 19:32:03 -0500 Subject: Respect newlines in commit messages on network graph --- app/assets/javascripts/network/branch_graph.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 64b19a54893..20a68780cd5 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -356,7 +356,7 @@ icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); nameText = this.text(x + 25, y + 10, commit.author.name); idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message); + messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); textSet = this.set(icon, nameText, idText, messageText).attr({ "text-anchor": "start", font: "12px Monaco, monospace" @@ -368,6 +368,7 @@ idText.attr({ fill: "#AAA" }); + messageText.node.style["white-space"] = "pre"; this.textWrap(messageText, boxWidth - 50); rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ fill: "#FFF", @@ -404,16 +405,21 @@ s.push("\n"); x = 0; } - x += word.length * letterWidth; - s.push(word + " "); + if (word === "\n") { + s.push("\n"); + x = 0; + } else { + s.push(word + " "); + x += word.length * letterWidth; + } } t.attr({ - text: s.join("") + text: s.join("").trim() }); b = t.getBBox(); - h = Math.abs(b.y2) - Math.abs(b.y) + 1; + h = Math.abs(b.y2) + 1; return t.attr({ - y: b.y + h + y: h }); }; -- cgit v1.2.1 From 24dd95756e267413a6f4e238eef06f8378dcaf63 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 17 Dec 2016 12:29:12 +0000 Subject: 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; /* -- cgit v1.2.1 From e1e677a653a1c6658ad7f3c9f4fbae7f120c8a8f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sat, 17 Dec 2016 12:55:26 +0000 Subject: Changes after review --- app/views/ci/status/_mini_graph_badge.html.haml | 5 ----- app/views/projects/ci/pipelines/_pipeline.html.haml | 13 +++++++------ 2 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 app/views/ci/status/_mini_graph_badge.html.haml diff --git a/app/views/ci/status/_mini_graph_badge.html.haml b/app/views/ci/status/_mini_graph_badge.html.haml deleted file mode 100644 index 34e07e75ae8..00000000000 --- a/app/views/ci/status/_mini_graph_badge.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- status = subject.detailed_status(current_user) -- icon = "#{status.icon}_borderless" -- klass = "ci-status-icon ci-status-icon-#{status}" - -%span{ class: klass }= custom_icon(icon) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 0651447f616..2e2bac9cd32 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -44,10 +44,11 @@ - pipeline.stages.each do |stage| - if stage.status - detailed_status = stage.detailed_status(current_user) - - details_path = detailed_status.details_path if detailed_status.has_details? - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status}" - hasMultipleBuilds = stage.statuses.count > 1 - - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" + - icon_status = "#{detailed_status.icon}_borderless" + - icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status}" + - tooltip = "#{stage.name}: #{detailed_status.label || 'not found'}" .stage-container.mini-pipeline-graph - if hasMultipleBuilds @@ -55,7 +56,7 @@ %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } %span{ class: klass } %span.mini-pipeline-graph-icon-container - = render 'ci/status/mini_graph_badge', subject: stage + %span{ class: icon_status_klass }= custom_icon(icon_status) = icon('caret-down', class: 'dropdown-caret') .dropdown-menu.grouped-pipeline-dropdown .arrow-up @@ -64,10 +65,10 @@ %li.dropdown-build = render 'ci/status/graph_badge', subject: status - else - - if details_path - = link_to details_path, class: klass, title: tooltip do + - if detailed_status.has_details? + = link_to detailed_status.details_path, class: klass, title: tooltip do %span.mini-pipeline-graph-icon-container - = render 'ci/status/mini_graph_badge', subject: stage + %span{ class: icon_status_klass }= custom_icon(icon_status) %td - if pipeline.duration -- cgit v1.2.1 From b23f32a735459f1c76ac8232f9de06942b1d2f8a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 17 Dec 2016 18:28:36 +0000 Subject: 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 -- cgit v1.2.1 From a701b1fcbbdc46918a53126bd59fa1c550e681dc Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 16 Dec 2016 18:13:52 +0000 Subject: 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 | 79 +++++++++--------- app/views/shared/icons/_mattermost_logo.svg.erb | 1 + 8 files changed, 169 insertions(+), 149 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 - - - field = @service.event_field(event) - - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - %p.light - = service_event_description(event) - - - @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 + = form.check_box :active + + - 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 + + - field = @service.event_field(event) + + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + + %p.light + = service_event_description(event) + + - @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 @@ + -- cgit v1.2.1 From 3f60a276fc36fc7d1c5323c38b33fdbc774cfbbf Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 13 Dec 2016 16:53:01 +0000 Subject: Added slack slash commands frontend help well Added tests --- app/assets/stylesheets/framework/forms.scss | 4 + .../services/slack_slash_commands/_help.html.haml | 93 ++++++++++++++++++++++ .../projects/services/slack_slash_command_spec.rb | 48 +++++++++++ 3 files changed, 145 insertions(+) create mode 100644 app/views/projects/services/slack_slash_commands/_help.html.haml create mode 100644 spec/features/projects/services/slack_slash_command_spec.rb diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 940807fc399..8726a69867b 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -96,6 +96,10 @@ label { code { line-height: 1.8; } + + img { + margin-right: $gl-padding; + } } @media(max-width: $screen-xs-max) { diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml new file mode 100644 index 00000000000..c45052e3954 --- /dev/null +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -0,0 +1,93 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +.well + This service allows GitLab users to perform common operations on this + project by entering slash commands in Slack. + %br + See list of available commands in Slack after setting up this service, + by entering + %code /<command> help + %br + %br + To setup this service: + %ul.list-unstyled + %li + 1. + = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' + in your Slack team with these options: + + %hr + + .help-form + .form-group + = label_tag nil, 'Command', 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 :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#url') + + .form-group + = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#customize_name') + + .form-group + = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + = image_tag(asset_url('gitlab_logo.png'), width: 36, height: 36) + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list + + .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') + + .form-group + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_usage_hint') + + .form-group + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#descriptive_label') + + %hr + + %ul.list-unstyled + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb new file mode 100644 index 00000000000..dee43d69895 --- /dev/null +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +feature 'Setup Slack slash commands', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:service) { project.create_slack_slash_commands_service } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'user visits the slack 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) + + fill_in 'service_token', with: token + click_on 'Save' + + value = find_field('service_token').value + + 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) + + value = find_field('url').value + expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") + end + end +end -- cgit v1.2.1 From 676b79b94d33e007b34d70c00900a52983ce1106 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 18 Dec 2016 23:30:37 +0100 Subject: Fix Rubocop --- lib/gitlab/chat_commands/presenter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index e94d0ce2470..b4c4dc252ca 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -43,7 +43,7 @@ module Gitlab end def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") end private @@ -88,7 +88,7 @@ module Gitlab reference = resource.try(:to_reference) || resource.try(:id) title = resource.try(:title) || resource.try(:name) - "[#{reference} #{title}](#{url(resource)})" + "[#{reference} #{title}](#{url(resource)})" end def header_with_list(header, items) -- cgit v1.2.1 From 9e3153dbf556b9b9397806bedcf0a195e8ee3fa4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 18 Dec 2016 23:30:48 +0100 Subject: Remove not related spec changes --- .../chat_message/build_message_spec.rb | 8 +++---- .../chat_message/issue_message_spec.rb | 10 ++++----- .../chat_message/merge_message_spec.rb | 12 +++++----- .../chat_message/note_message_spec.rb | 22 +++++++++--------- .../chat_message/push_message_spec.rb | 26 +++++++++++----------- .../chat_message/wiki_page_message_spec.rb | 8 +++---- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 50ad5013df9..b71d153f814 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'http://example.gitlab.com', + project_url: 'example.gitlab.com', commit: { status: status, @@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do end def build_message(status_text = status) - ":" \ - " Commit :" \ + " Commit " \ - " of branch" \ + " of branch" \ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb index 190ff4c535d..ebe0ead4408 100644 --- a/spec/models/project_services/chat_message/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'http://somewhere.com', + project_url: 'somewhere.com', object_attributes: { title: 'Issue title', id: 10, iid: 100, assignee_id: 1, - url: 'http://url.com', + url: 'url', action: 'open', state: 'opened', description: 'issue description' @@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - '[] Issue opened by test.user') + '] Issue opened by test.user') expect(subject.attachments).to eq([ { title: "#100 Issue title", - title_link: "http://url.com", + title_link: "url", text: "issue description", color: color, } @@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - '[] Issue closed by test.user') + '] Issue closed by test.user') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index cc154112e90..07c414c6ca4 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'http://somewhere.com', + project_url: 'somewhere.com', object_attributes: { title: "Issue title\nSecond line", id: 10, iid: 100, assignee_id: 1, - url: 'http://url.com', + url: 'url', state: 'opened', description: 'issue description', source_branch: 'source_branch', @@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'test.user opened '\ - 'in : *Issue title*') + 'test.user opened '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end @@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'test.user closed '\ - 'in : *Issue title*') + 'test.user closed '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb index da700a08e57..31936da40a2 100644 --- a/spec/models/project_services/chat_message/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do avatar_url: 'http://fakeavatar' }, project_name: 'project_name', - project_url: 'http://somewhere.com', + project_url: 'somewhere.com', repository: { name: 'project_name', - url: 'http://somewhere.com', + url: 'somewhere.com', }, object_attributes: { id: 10, note: 'comment on a commit', - url: 'http://url.com', + url: 'url', noteable_type: 'Commit' } } @@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on commits' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*Added a commit message*") expected_attachments = [ { @@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*merge request title*") expected_attachments = [ { @@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = described_class.new(@args) expect(message.pretext).to eq( - "test.user in : " \ + "test.user in : " \ "*issue title*") expected_attachments = [ { @@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a project snippet' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*snippet title*") expected_attachments = [ { diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index 24928873bad..b781c4505db 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/heads/master', user_name: 'test.user', - project_url: 'http://url.com' + project_url: 'url' } end @@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do context 'push' do before do args[:commits] = [ - { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }, + { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, ] end it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch of '\ - ' ()' + 'test.user pushed to branch of '\ + ' ()' ) expect(subject.attachments).to eq([ { - text: ": message1 - author1\n"\ - ": message2 - author2", + text: ": message1 - author1\n"\ + ": message2 - author2", color: color, } ]) @@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/tags/new_tag', user_name: 'test.user', - project_url: 'http://url.com' + project_url: 'url' } end it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - ' to ' \ - '') + ' to ' \ + '') expect(subject.attachments).to be_empty end end @@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch to '\ - '' + 'test.user pushed new branch to '\ + '' ) expect(subject.attachments).to be_empty end @@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch master from ' + 'test.user removed branch master from ' ) expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index a2ad61e38e7..94c04dc0865 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'http://somewhere.com', + project_url: 'somewhere.com', object_attributes: { title: 'Wiki page title', - url: 'http://url.com', + url: 'url', content: 'Wiki page description' } } @@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( - 'test.user created in : '\ + 'test.user created in : '\ '*Wiki page title*') end end @@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( - 'test.user edited in : '\ + 'test.user edited in : '\ '*Wiki page title*') end end -- cgit v1.2.1 From f9023adbad4939ba597d509e319105659e61734b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 18 Dec 2016 23:32:53 +0100 Subject: Fix spec failures --- app/models/project_services/slack_slash_commands_service.rb | 2 +- spec/features/admin/admin_settings_spec.rb | 4 ++-- spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/project_spec.rb | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 8413c657099..6782ba5ad0a 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -2,7 +2,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService include TriggersHelper def title - 'Slack Slash Command' + 'Slack Command' end def description diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 8cd66f189be..e7a23746244 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -17,9 +17,9 @@ feature 'Admin updates settings', feature: true do expect(page).to have_content "Application settings saved successfully" end - scenario 'Change Slack Service template settings' do + scenario 'Change Slack Notifications Service template settings' do click_link 'Service Templates' - click_link 'Slack' + click_link 'Slack notifications' fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' fill_in 'service_push_channel', with: '#test_channel' diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 9b49d6837c3..7e618e2fcf5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -129,6 +129,7 @@ project: - builds_email_service - pipelines_email_service - mattermost_slash_commands_service +- slack_slash_commands_service - irker_service - pivotaltracker_service - hipchat_service diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bab3c3dbb02..4b39dc77f29 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -20,7 +20,6 @@ describe Project, models: true do it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } - it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } @@ -37,6 +36,7 @@ describe Project, models: true do it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } it { is_expected.to have_one(:assembla_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } -- cgit v1.2.1 From f5ff372140d066d7bcedc0ad0799c723a9012bb0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 18 Dec 2016 23:34:40 +0100 Subject: Fix failures --- spec/features/admin/admin_settings_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index e7a23746244..47fa2f14307 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -30,7 +30,7 @@ feature 'Admin updates settings', feature: true do expect(page).to have_content 'Application settings saved successfully' - click_link 'Slack' + click_link 'Slack notifications' page.all('input[type=checkbox]').each do |checkbox| expect(checkbox).to be_checked -- cgit v1.2.1 From 3cc334eae5abf65b0ecb4159e4e9712dab81c33c Mon Sep 17 00:00:00 2001 From: Arsenev Vladislav Date: Sun, 18 Dec 2016 21:56:13 +0000 Subject: remove build_user from model User --- app/models/user.rb | 4 ---- changelogs/unreleased/25678-remove-user-build.yml | 4 ++++ lib/api/users.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25678-remove-user-build.yml diff --git a/app/models/user.rb b/app/models/user.rb index 3a17c98eff6..d771eaff472 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -311,10 +311,6 @@ class User < ActiveRecord::Base find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) end - def build_user(attrs = {}) - User.new(attrs) - end - def reference_prefix '@' end diff --git a/changelogs/unreleased/25678-remove-user-build.yml b/changelogs/unreleased/25678-remove-user-build.yml new file mode 100644 index 00000000000..873e637d670 --- /dev/null +++ b/changelogs/unreleased/25678-remove-user-build.yml @@ -0,0 +1,4 @@ +--- +title: remove build_user +merge_request: 8162 +author: Arsenev Vladislav diff --git a/lib/api/users.rb b/lib/api/users.rb index 0842c3874c5..4c22287b5c6 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -94,7 +94,7 @@ module API identity_attrs = params.slice(:provider, :extern_uid) confirm = params.delete(:confirm) - user = User.build_user(declared_params(include_missing: false)) + user = User.new(declared_params(include_missing: false)) user.skip_confirmation! unless confirm if identity_attrs.any? -- cgit v1.2.1 From 64d7772b6f0594896eb1ac67d5d3f4c33c813fe3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 18:43:06 +0800 Subject: Use a separate method to skip validation Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8091#note_20222666 --- lib/ci/api/builds.rb | 2 +- lib/ci/api/helpers.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 3c4cfccb19a..142bce82286 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -41,7 +41,7 @@ module Ci put ":id" do authenticate_runner! build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) - authenticate_build!(build, verify_token: false) + validate_build!(build) update_runner_info diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 0202b3cf8a3..51b05aa0cb6 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -13,9 +13,14 @@ module Ci forbidden! unless current_runner end - def authenticate_build!(build, verify_token: true) + def authenticate_build!(build) + not_found! unless build + forbidden! if !build_token_valid?(build) + validate_build!(build) + end + + def validate_build!(build) not_found! unless build - forbidden! if verify_token && !build_token_valid?(build) forbidden!('Project has been deleted!') unless build.project forbidden!('Build has been erased!') if build.erased? end -- cgit v1.2.1 From fb23153343f274a29cba1023759f675aaf64251a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 18:50:01 +0800 Subject: Delete the project when building the build Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8091#note_20222756 --- spec/requests/ci/api/builds_spec.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index d61a9afd12e..2963fe85478 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -332,21 +332,18 @@ describe Ci::API::Builds do context 'when project for the build has been deleted' do let(:build) do - create(:ci_build, - :pending, - :trace, - runner_id: runner.id, - pipeline: pipeline) + result = create(:ci_build, + :pending, + :trace, + runner_id: runner.id, + pipeline: pipeline) + result.project.update(pending_delete: true) + result end it 'responds with forbidden' do expect(response.status).to eq 403 end - - def initial_patch_the_trace - build.project.update(pending_delete: true) - super - end end end -- cgit v1.2.1 From ec003d9eb338f9172696f5540637e759a93f9fcf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 19:14:21 +0800 Subject: Prefer unless over if not Feedback: https://gitlab.com/gitlab-org/gitlab-ce/builds/7606797 --- lib/ci/api/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 51b05aa0cb6..62c10c3b753 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -15,7 +15,7 @@ module Ci def authenticate_build!(build) not_found! unless build - forbidden! if !build_token_valid?(build) + forbidden! unless build_token_valid?(build) validate_build!(build) end -- cgit v1.2.1 From 083e185cdaee03e66dec0ab267a2f3b5d3dab9a7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 13:20:17 +0100 Subject: Render stage dropdown in separate API call as HTML ``` Endpoint: /group/project/pipelines/id/stage.json?stage=name Call: stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) ``` --- app/controllers/projects/pipelines_controller.rb | 12 ++++++++++++ app/views/projects/pipelines/_stage.html.haml | 13 +++++++++++++ config/routes/project.rb | 1 + 3 files changed, 26 insertions(+) create mode 100644 app/views/projects/pipelines/_stage.html.haml diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 85188cfdd4c..0147072b0f1 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) + @pipelines = @pipelines.includes(project: :namespace) @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count @pipelines_count = PipelinesFinder.new(project).execute.count @@ -40,6 +41,17 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def stage + @stage = pipeline.stages.find do |stage| + stage.name == params[:stage] + end + return not_found unless @stage + + respond_to do |format| + format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } + end + end + def retry pipeline.retry_failed(current_user) diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml new file mode 100644 index 00000000000..44533b77eba --- /dev/null +++ b/app/views/projects/pipelines/_stage.html.haml @@ -0,0 +1,13 @@ +- detailed_status = @stage.detailed_status(current_user) +- klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status}" +- hasMultipleBuilds = @stage.statuses.count > 1 +- icon_status = "#{detailed_status.icon}_borderless" +- icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status}" +- tooltip = "#{@stage.name}: #{detailed_status.label || 'not found'}" + +.dropdown.inline.build-content + %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } + %span{ class: klass } + %span.mini-pipeline-graph-icon-container + %span{ class: icon_status_klass }= custom_icon(icon_status) + = icon('caret-down', class: 'dropdown-caret') diff --git a/config/routes/project.rb b/config/routes/project.rb index 0754f0ec3b0..909794922ce 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -127,6 +127,7 @@ constraints(ProjectUrlConstrainer.new) do end member do + get :stage post :cancel post :retry get :builds -- cgit v1.2.1 From e6842ff5643c7fc88a65af24b56682f47f4be2de Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 13:32:37 +0100 Subject: Improve code design --- app/models/project_services/chat_slash_commands_service.rb | 2 +- app/models/project_services/slack_slash_commands_service.rb | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 12261e9821e..0bc160af604 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,7 +28,7 @@ class ChatSlashCommandsService < Service end def trigger(params) - return nil unless valid_token?(params[:token]) + return unless valid_token?(params[:token]) user = find_chat_user(params) unless user diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 6782ba5ad0a..cb19ebf4cad 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -14,13 +14,15 @@ class SlackSlashCommandsService < ChatSlashCommandsService end def trigger(params) - result = super - # Format messages to be Slack-compatible - if result && result[:text] - result[:text] = Slack::Notifier::LinkFormatter.format(result[:text]) + super.tap do |result| + result[:text] = format(result[:text]) end + end + + private - result + def format(text) + Slack::Notifier::LinkFormatter.format(text) if text end end -- cgit v1.2.1 From b1ccf99e87605216f7d5733d6a4ffb4530d4cfc9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 13:34:03 +0100 Subject: Fix previously reverted spec failures --- .../chat_message/build_message_spec.rb | 8 +++---- .../chat_message/issue_message_spec.rb | 10 ++++----- .../chat_message/merge_message_spec.rb | 12 +++++----- .../chat_message/note_message_spec.rb | 22 +++++++++--------- .../chat_message/pipeline_message_spec.rb | 8 +++---- .../chat_message/push_message_spec.rb | 26 +++++++++++----------- .../chat_message/wiki_page_message_spec.rb | 8 +++---- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index b71d153f814..50ad5013df9 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'example.gitlab.com', + project_url: 'http://example.gitlab.com', commit: { status: status, @@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do end def build_message(status_text = status) - ":" \ - " Commit :" \ + " Commit " \ - " of branch" \ + " of branch" \ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb index ebe0ead4408..190ff4c535d 100644 --- a/spec/models/project_services/chat_message/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Issue title', id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', action: 'open', state: 'opened', description: 'issue description' @@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - '] Issue opened by test.user') + '[] Issue opened by test.user') expect(subject.attachments).to eq([ { title: "#100 Issue title", - title_link: "url", + title_link: "http://url.com", text: "issue description", color: color, } @@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - '] Issue closed by test.user') + '[] Issue closed by test.user') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index 07c414c6ca4..cc154112e90 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: "Issue title\nSecond line", id: 10, iid: 100, assignee_id: 1, - url: 'url', + url: 'http://url.com', state: 'opened', description: 'issue description', source_branch: 'source_branch', @@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'test.user opened '\ - 'in : *Issue title*') + 'test.user opened '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end @@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'test.user closed '\ - 'in : *Issue title*') + 'test.user closed '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb index 31936da40a2..da700a08e57 100644 --- a/spec/models/project_services/chat_message/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do avatar_url: 'http://fakeavatar' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', repository: { name: 'project_name', - url: 'somewhere.com', + url: 'http://somewhere.com', }, object_attributes: { id: 10, note: 'comment on a commit', - url: 'url', + url: 'http://url.com', noteable_type: 'Commit' } } @@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on commits' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*Added a commit message*") expected_attachments = [ { @@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*merge request title*") expected_attachments = [ { @@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = described_class.new(@args) expect(message.pretext).to eq( - "test.user in : " \ + "test.user in : " \ "*issue title*") expected_attachments = [ { @@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do it 'returns a message regarding notes on a project snippet' do message = described_class.new(@args) - expect(message.pretext).to eq("test.user in : " \ + expect(message.pretext).to eq("test.user in : " \ "*snippet title*") expected_attachments = [ { diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index eca71db07b6..bf2a9616455 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -15,7 +15,7 @@ describe ChatMessage::PipelineMessage do duration: duration }, project: { path_with_namespace: 'project_name', - web_url: 'example.gitlab.com' }, + web_url: 'http://example.gitlab.com' }, user: user } end @@ -59,9 +59,9 @@ describe ChatMessage::PipelineMessage do end def build_message(status_text = status, name = user[:name]) - ":" \ - " Pipeline " \ - " of branch" \ + ":" \ + " Pipeline " \ + " of branch" \ " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index b781c4505db..24928873bad 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/heads/master', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end @@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do context 'push' do before do args[:commits] = [ - { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }, ] end it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch of '\ - ' ()' + 'test.user pushed to branch of '\ + ' ()' ) expect(subject.attachments).to eq([ { - text: ": message1 - author1\n"\ - ": message2 - author2", + text: ": message1 - author1\n"\ + ": message2 - author2", color: color, } ]) @@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do project_name: 'project_name', ref: 'refs/tags/new_tag', user_name: 'test.user', - project_url: 'url' + project_url: 'http://url.com' } end it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - ' to ' \ - '') + ' to ' \ + '') expect(subject.attachments).to be_empty end end @@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch to '\ - '' + 'test.user pushed new branch to '\ + '' ) expect(subject.attachments).to be_empty end @@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch master from ' + 'test.user removed branch master from ' ) expect(subject.attachments).to be_empty end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index 94c04dc0865..a2ad61e38e7 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do username: 'test.user' }, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'http://somewhere.com', object_attributes: { title: 'Wiki page title', - url: 'url', + url: 'http://url.com', content: 'Wiki page description' } } @@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( - 'test.user created in : '\ + 'test.user created in : '\ '*Wiki page title*') end end @@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( - 'test.user edited in : '\ + 'test.user edited in : '\ '*Wiki page title*') end end -- cgit v1.2.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: 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 - describe '.create' do - let(:new_command) do - JSON.parse(File.read(Rails.root.join('spec/fixtures/', 'mattermost_new_command.json'))) - end + let(:session) { double("session") } + describe '.create' do 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') + allow(session).to receive(:post).and_return('token' => 'token') + expect(session).to receive(:post) - 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 -- cgit v1.2.1 From 1e2e0de9441a2f9777bb989e8a8c275c2b103ca7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 22:01:37 +0800 Subject: Define actions in let so that it could be overridden Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8091/diffs#note_20236356 --- spec/requests/ci/api/builds_spec.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 2963fe85478..fdb2234d32b 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -249,7 +249,13 @@ describe Ci::API::Builds do end describe 'PATCH /builds/:id/trace.txt' do - let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) } + let(:build) do + attributes = {runner_id: runner.id, pipeline: pipeline} + create(:ci_build, :pending, :trace, attributes) do |build| + build.run + end + end + let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } let(:update_interval) { 10.seconds.to_i } @@ -276,7 +282,6 @@ describe Ci::API::Builds do end before do - build.run! initial_patch_the_trace end @@ -332,17 +337,15 @@ describe Ci::API::Builds do context 'when project for the build has been deleted' do let(:build) do - result = create(:ci_build, - :pending, - :trace, - runner_id: runner.id, - pipeline: pipeline) - result.project.update(pending_delete: true) - result + attributes = {runner_id: runner.id, pipeline: pipeline} + create(:ci_build, :pending, :trace, attributes) do |build| + build.run + build.project.update(pending_delete: true) + end end it 'responds with forbidden' do - expect(response.status).to eq 403 + expect(response.status).to eq(403) end end end -- cgit v1.2.1 From 0aa61e8e6f7d528330a50e64bf27b2d721c4ec5a Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 19 Dec 2016 20:11:43 +0600 Subject: hides new issue btn for now loggedin user --- 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 e939278bc07..07d4927b6c9 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -8,7 +8,7 @@ = render 'shared/empty_states/icons/issues.svg' .col-xs-12{ class: "#{'col-sm-6' if has_button}" } .text-content - - if has_button + - if has_button && current_user %h4 The Issue Tracker is a good place to add things that need to be improved or solved in a project! %p -- cgit v1.2.1 From e6c83b1c1f3db5cea9730ae320f3a6525fd6fe2a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 22:19:03 +0800 Subject: Just set the status rather than calling event Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8091#note_20239558 --- spec/requests/ci/api/builds_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index fdb2234d32b..5acda0fd729 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -251,9 +251,7 @@ describe Ci::API::Builds do describe 'PATCH /builds/:id/trace.txt' do let(:build) do attributes = {runner_id: runner.id, pipeline: pipeline} - create(:ci_build, :pending, :trace, attributes) do |build| - build.run - end + create(:ci_build, :running, :trace, attributes) end let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } @@ -338,8 +336,7 @@ describe Ci::API::Builds do context 'when project for the build has been deleted' do let(:build) do attributes = {runner_id: runner.id, pipeline: pipeline} - create(:ci_build, :pending, :trace, attributes) do |build| - build.run + create(:ci_build, :running, :trace, attributes) do |build| build.project.update(pending_delete: true) end end -- cgit v1.2.1 From 53aeb33f46ec8078dc4cdbdbbd509e091d7c04bf Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 14:21:39 +0000 Subject: 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 | 79 ++++++++++--------- 6 files changed, 146 insertions(+), 59 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 - - - 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 - - - field = @service.event_field(event) - - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - %p.light - = service_event_description(event) - - - @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 + - @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) + + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + + %p.light + = service_event_description(event) + + - @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 -- cgit v1.2.1 From ad39831049a5d645da2df62de4f9a618fa6f80d7 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Mon, 19 Dec 2016 16:29:58 +0500 Subject: Move admin projects spinach tests to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/admin/projects.feature | 47 ------------- features/steps/admin/projects.rb | 104 ----------------------------- spec/features/admin/admin_projects_spec.rb | 99 ++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 159 deletions(-) delete mode 100644 features/admin/projects.feature delete mode 100644 features/steps/admin/projects.rb diff --git a/features/admin/projects.feature b/features/admin/projects.feature deleted file mode 100644 index 8929bcf8d80..00000000000 --- a/features/admin/projects.feature +++ /dev/null @@ -1,47 +0,0 @@ -@admin -Feature: Admin Projects - Background: - Given I sign in as an admin - And there are projects in system - - Scenario: I should see non-archived projects in the list - Given archived project "Archive" - When I visit admin projects page - Then I should see all non-archived projects - And I should not see project "Archive" - - @javascript - Scenario: I should see all projects in the list - Given archived project "Archive" - When I visit admin projects page - And I select "Show archived projects" - Then I should see all projects - And I should see "archived" label - - Scenario: Projects show - When I visit admin projects page - And I click on first project - Then I should see project details - - @javascript - Scenario: Transfer project - Given group 'Web' - And I visit admin project page - When I transfer project to group 'Web' - Then I should see project transfered - - @javascript - Scenario: Signed in admin should be able to add himself to a project - Given "John Doe" owns private project "Enterprise" - When I visit project "Enterprise" members page - When I select current user as "Developer" - Then I should see current user as "Developer" - - @javascript - Scenario: Signed in admin should be able to remove himself from a project - Given "John Doe" owns private project "Enterprise" - And current user is developer of project "Enterprise" - When I visit project "Enterprise" members page - Then I should see current user as "Developer" - When I click on the "Remove User From Project" button for current user - Then I should not see current user as "Developer" diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb deleted file mode 100644 index 2b8cd030ace..00000000000 --- a/features/steps/admin/projects.rb +++ /dev/null @@ -1,104 +0,0 @@ -class Spinach::Features::AdminProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - include SharedProject - include SharedUser - include Select2Helper - - step 'I should see all non-archived projects' do - Project.non_archived.each do |p| - expect(page).to have_content p.name_with_namespace - end - end - - step 'I should see all projects' do - Project.all.each do |p| - expect(page).to have_content p.name_with_namespace - end - end - - step 'I select "Show archived projects"' do - find(:css, '#sort-projects-dropdown').click - click_link 'Show archived projects' - end - - step 'I should see "archived" label' do - expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') - end - - step 'I click on first project' do - click_link Project.first.name_with_namespace - end - - step 'I should see project details' do - project = Project.first - expect(current_path).to eq admin_namespace_project_path(project.namespace, project) - expect(page).to have_content(project.name_with_namespace) - expect(page).to have_content(project.creator.name) - end - - step 'I visit admin project page' do - visit admin_namespace_project_path(project.namespace, project) - end - - step 'I transfer project to group \'Web\'' do - allow_any_instance_of(Projects::TransferService). - to receive(:move_uploads_to_new_namespace).and_return(true) - click_button 'Search for Namespace' - click_link 'group: web' - click_button 'Transfer' - end - - step 'group \'Web\'' do - create(:group, name: 'Web') - end - - step 'I should see project transfered' do - expect(page).to have_content 'Web / ' + project.name - expect(page).to have_content 'Namespace: Web' - end - - step 'I visit project "Enterprise" members page' do - project = Project.find_by!(name: "Enterprise") - visit namespace_project_project_members_path(project.namespace, project) - end - - step 'I select current user as "Developer"' do - page.within ".users-project-form" do - select2(current_user.id, from: "#user_ids", multiple: true) - select "Developer", from: "access_level" - end - - click_button "Add to project" - end - - step 'I should see current user as "Developer"' do - page.within '.content-list' do - expect(page).to have_content(current_user.name) - expect(page).to have_content('Developer') - end - end - - step 'current user is developer of project "Enterprise"' do - project = Project.find_by!(name: "Enterprise") - project.team << [current_user, :developer] - end - - step 'I click on the "Remove User From Project" button for current user' do - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see current_user as "Developer"' do - expect(page).not_to have_selector(:css, '.content-list') - end - - def project - @project ||= Project.first - end - - def group - Group.find_by(name: 'Web') - end -end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index a36bfd574cb..a5b88812b75 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,12 +1,17 @@ require 'spec_helper' describe "Admin::Projects", feature: true do - before do - @project = create(:project) + include Select2Helper + + let(:user) { create :user } + let!(:project) { create(:project) } + let!(:current_user) do login_as :admin end describe "GET /admin/projects" do + let!(:archived_project) { create :project, :public, archived: true } + before do visit admin_projects_path end @@ -15,20 +20,98 @@ describe "Admin::Projects", feature: true do expect(current_path).to eq(admin_projects_path) end - it "has projects list" do - expect(page).to have_content(@project.name) + it 'renders projects list without archived project' do + expect(page).to have_content(project.name) + expect(page).not_to have_content(archived_project.name) + end + + it 'renders all projects', js: true do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' + + expect(page).to have_content(project.name) + expect(page).to have_content(archived_project.name) + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end end - describe "GET /admin/projects/:id" do + describe "GET /admin/projects/:namespace_id/:id" do before do visit admin_projects_path - click_link "#{@project.name}" + click_link "#{project.name}" + end + + it do + expect(current_path).to eq admin_namespace_project_path(project.namespace, project) end it "has project info" do - expect(page).to have_content(@project.path) - expect(page).to have_content(@project.name) + expect(page).to have_content(project.path) + expect(page).to have_content(project.name) + expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.creator.name) + end + end + + describe 'transfer project' do + before do + create(:group, name: 'Web') + + allow_any_instance_of(Projects::TransferService). + to receive(:move_uploads_to_new_namespace).and_return(true) + end + + it 'transfers project to group web', js: true do + visit admin_namespace_project_path(project.namespace, project) + + click_button 'Search for Namespace' + click_link 'group: web' + click_button 'Transfer' + + expect(page).to have_content("Web / #{project.name}") + expect(page).to have_content('Namespace: Web') + end + end + + describe 'add admin himself to a project' do + before do + project.team << [user, :master] + end + + it 'adds admin a to a project as developer', js: true do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.users-project-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Add to project' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin remove himself from a project' do + before do + project.team << [user, :master] + project.team << [current_user, :developer] + end + + it 'removes admin from the project' do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + + expect(page).not_to have_selector(:css, '.content-list') end end end -- cgit v1.2.1 From 92af7bfc25bf540fcdd295b3ecfacfa24c5e6d9f Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Mon, 19 Dec 2016 20:35:40 +0600 Subject: adds changelog files --- changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml 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 new file mode 100644 index 00000000000..18836e7a90b --- /dev/null +++ b/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml @@ -0,0 +1,4 @@ +--- +title: Hides new issue button for non loggedin user +merge_request: 8175 +author: -- cgit v1.2.1 From 298d05a5c3cc3c2f1daa4d77c45f9c90b53248df Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 15:40:06 +0100 Subject: Improve after feedback --- features/project/service.feature | 6 ++--- features/steps/project/services.rb | 8 +++---- lib/api/services.rb | 9 ++++++- lib/gitlab/chat_commands/help.rb | 28 ---------------------- lib/gitlab/chat_commands/presenter.rb | 2 +- .../projects/services/slack_slash_command_spec.rb | 18 +++++++------- .../mattermost_slash_commands_service_spec.rb | 4 ++-- .../slack_slash_commands_service.rb | 6 +++-- .../support/chat_slash_commands_shared_examples.rb | 2 +- 9 files changed, 32 insertions(+), 51 deletions(-) delete mode 100644 lib/gitlab/chat_commands/help.rb diff --git a/features/project/service.feature b/features/project/service.feature index 3a7b8308524..892db48d785 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -39,9 +39,9 @@ Feature: Project Services Scenario: Activate Slack service When I visit project "Shop" services page - And I click Slack service link - And I fill Slack settings - Then I should see Slack service settings saved + And I click Slack Notifications service link + And I fill Slack Notifications settings + Then I should see Slack Notifications service settings saved Scenario: Activate Pushover service When I visit project "Shop" services page diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index bd6466f3686..06a1afedbd9 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -137,17 +137,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Colorize messages').value).to eq '1' end - step 'I click Slack service link' do - click_link 'Slack' + step 'I click Slack Notifications service link' do + click_link 'Slack Notifications' end - step 'I fill Slack settings' do + step 'I fill Slack Notifications settings' do check 'Active' fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' click_button 'Save' end - step 'I should see Slack service settings saved' do + step 'I should see Slack Notifications service settings saved' do expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' end diff --git a/lib/api/services.rb b/lib/api/services.rb index 59232c84c24..aa97f6af0b2 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -378,7 +378,6 @@ module API desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' }, ], - 'mattermost-slash-commands' => [ { required: true, @@ -387,6 +386,14 @@ module API desc: 'The Mattermost token' } ], + 'slack-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Slack token' + } + ], 'pipelines-email' => [ { required: true, diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb deleted file mode 100644 index e76733f5445..00000000000 --- a/lib/gitlab/chat_commands/help.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Gitlab - module ChatCommands - class Help < BaseCommand - # This class has to be used last, as it always matches. It has to match - # because other commands were not triggered and we want to show the help - # command - def self.match(_text) - true - end - - def self.help_message - 'help' - end - - def self.allowed?(_project, _user) - true - end - - def execute(commands) - Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) - end - - def trigger - params[:command] - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb index b4c4dc252ca..caceaa25391 100644 --- a/lib/gitlab/chat_commands/presenter.rb +++ b/lib/gitlab/chat_commands/presenter.rb @@ -1,7 +1,7 @@ module Gitlab module ChatCommands class Presenter - include Gitlab::Routing.url_helpers + include Gitlab::Routing def authorize_chat_name(url) message = if url diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index dee43d69895..70e203efcf5 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -1,18 +1,18 @@ require 'spec_helper' -feature 'Setup Slack slash commands', feature: true do +feature 'Slack slash commands', feature: true do include WaitForAjax - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:service) { project.create_slack_slash_commands_service } + given(:user) { create(:user) } + given(:project) { create(:project) } + given(:service) { project.create_slack_slash_commands_service } - before do + background do project.team << [user, :master] login_as(user) end - describe 'user visits the slack slash command config page', js: true do + scenario 'user visits the slack slash command config page', js: true do it 'shows a help message' do visit edit_namespace_project_service_path(project.namespace, project, service) @@ -22,8 +22,8 @@ feature 'Setup Slack slash commands', feature: true do end end - describe 'saving a token' do - let(:token) { ('a'..'z').to_a.join } + scenario 'saving a token' do + given(:token) { ('a'..'z').to_a.join } it 'shows the token after saving' do visit edit_namespace_project_service_path(project.namespace, project, service) @@ -37,7 +37,7 @@ feature 'Setup Slack slash commands', feature: true do end end - describe 'the trigger url' do + scenario 'the trigger url' do it 'shows the correct url' do visit edit_namespace_project_service_path(project.namespace, project, service) 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 5c34cb6b4cf..1ae1483e2a4 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -describe MattermostSlashCommandsService, models: true do - it_behaves_like "chat slash commands" +describe MattermostSlashCommandsService, :models do + it_behaves_like "chat slash commands service" end diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service.rb index c3fa80caebe..5775e439906 100644 --- a/spec/models/project_services/slack_slash_commands_service.rb +++ b/spec/models/project_services/slack_slash_commands_service.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackSlashCommandsService, models: true do - it_behaves_like "chat slash commands" +describe SlackSlashCommandsService, :models do + it_behaves_like "chat slash commands service" describe '#trigger' do context 'when an auth url is generated' do @@ -15,11 +15,13 @@ describe SlackSlashCommandsService, models: true do token: 'token' } end + let(:service) do project.create_slack_slash_commands_service( properties: { token: 'token' } ) end + let(:authorize_url) do 'http://authorize.example.com/' end diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb index 96130b45235..4dfa29849ee 100644 --- a/spec/support/chat_slash_commands_shared_examples.rb +++ b/spec/support/chat_slash_commands_shared_examples.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples 'chat slash commands' do +RSpec.shared_examples 'chat slash commands service' do describe "Associations" do it { is_expected.to respond_to :token } it { is_expected.to have_many :chat_names } -- cgit v1.2.1 From 9ee6e5252ecb0a6a17c3d6336e8fb824153ad626 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 14:57:03 +0000 Subject: 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 -- cgit v1.2.1 From 6a43111f45dbbbf2f304c9b115a89ded35124626 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:02:49 +0000 Subject: Removed UI to show the registered team as its not yet possible --- .../services/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 -- cgit v1.2.1 From 8df4a4124d6f30c63ad93ae55a75cf37cc494a4d Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:08:16 +0000 Subject: Remove mock emoji comments --- .../services/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 -- cgit v1.2.1 From 7269df2822de786716636f757dee0f3780c5b08b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 19 Dec 2016 15:10:00 +0000 Subject: Use group name for css classes --- app/views/projects/ci/pipelines/_pipeline.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2e2bac9cd32..74ad9557130 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -44,10 +44,10 @@ - pipeline.stages.each do |stage| - if stage.status - detailed_status = stage.detailed_status(current_user) - - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status}" + - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status.group}" - hasMultipleBuilds = stage.statuses.count > 1 - icon_status = "#{detailed_status.icon}_borderless" - - icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status}" + - icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - tooltip = "#{stage.name}: #{detailed_status.label || 'not found'}" .stage-container.mini-pipeline-graph -- cgit v1.2.1 From a0690c4c01fceccef9efca70a9256f5790fba9c7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 23:20:22 +0800 Subject: Spaces around literal hash Feedback: https://gitlab.com/gitlab-org/gitlab-ce/builds/7617209 --- spec/requests/ci/api/builds_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 5acda0fd729..79f12ace999 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -250,7 +250,7 @@ describe Ci::API::Builds do describe 'PATCH /builds/:id/trace.txt' do let(:build) do - attributes = {runner_id: runner.id, pipeline: pipeline} + attributes = { runner_id: runner.id, pipeline: pipeline } create(:ci_build, :running, :trace, attributes) end @@ -335,7 +335,7 @@ describe Ci::API::Builds do context 'when project for the build has been deleted' do let(:build) do - attributes = {runner_id: runner.id, pipeline: pipeline} + attributes = { runner_id: runner.id, pipeline: pipeline } create(:ci_build, :running, :trace, attributes) do |build| build.project.update(pending_delete: true) end -- cgit v1.2.1 From 9d7744594f7fd62ccfdb003f069596437465f7f2 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 15:56:39 +0000 Subject: zj frontend review --- app/views/projects/mattermost/new.html.haml | 5 ++--- .../services/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 -- cgit v1.2.1 From f73193c328b871a9a3af803012c10d9bc1bd0904 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 6 Dec 2016 17:31:58 +0100 Subject: Smarter refreshing of authorized projects Prior to this commit the refreshing of authorized projects was done in two steps: 1. Remove existing authorizations 2. Insert a new list of all authorizations This can lead to a high amount of dead tuples as every time all rows are being replaced. For example, if a user with 100 authorizations is given access to a new project this would lead to: * 100 rows being removed * 101 new rows being inserted This commit changes the way this system works so it only removes/inserts what is necessary. Using the above example this would lead to only 1 new row being inserted, with the initial 100 being left untouched. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/25257 --- app/models/project_authorization.rb | 13 ++ app/models/user.rb | 38 ++--- .../users/refresh_authorized_projects_service.rb | 128 ++++++++++++++ app/workers/authorized_projects_worker.rb | 22 +-- spec/models/project_authorization_spec.rb | 25 +++ .../refresh_authorized_projects_service_spec.rb | 185 +++++++++++++++++++++ spec/workers/authorized_projects_worker_spec.rb | 14 +- 7 files changed, 364 insertions(+), 61 deletions(-) create mode 100644 app/services/users/refresh_authorized_projects_service.rb create mode 100644 spec/models/project_authorization_spec.rb create mode 100644 spec/services/users/refresh_authorized_projects_service_spec.rb diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index a00d43773d9..4c7f4f5a429 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base validates :project, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true + + def self.insert_authorizations(rows, per_batch = 1000) + rows.each_slice(per_batch) do |slice| + tuples = slice.map do |tuple| + tuple.map { |value| connection.quote(value) } + end + + connection.execute <<-EOF.strip_heredoc + INSERT INTO project_authorizations (user_id, project_id, access_level) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3a17c98eff6..9f5cc149361 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -443,22 +443,16 @@ class User < ActiveRecord::Base end def refresh_authorized_projects - transaction do - project_authorizations.delete_all - - # project_authorizations_union can return multiple records for the same - # project/user with different access_level so we take row with the maximum - # access_level - project_authorizations.connection.execute <<-SQL - INSERT INTO project_authorizations (user_id, project_id, access_level) - SELECT user_id, project_id, MAX(access_level) AS access_level - FROM (#{project_authorizations_union.to_sql}) sub - GROUP BY user_id, project_id - SQL - - unless authorized_projects_populated - update_column(:authorized_projects_populated, true) - end + Users::RefreshAuthorizedProjectsService.new(self).execute + end + + def remove_project_authorizations(project_ids) + project_authorizations.where(id: project_ids).delete_all + end + + def set_authorized_projects_column + unless authorized_projects_populated + update_column(:authorized_projects_populated, true) end end @@ -905,18 +899,6 @@ class User < ActiveRecord::Base private - # Returns a union query of projects that the user is authorized to access - def project_authorizations_union - relations = [ - personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - groups_projects.select_for_project_authorization, - projects.select_for_project_authorization, - groups.joins(:shared_projects).select_for_project_authorization - ] - - Gitlab::SQL::Union.new(relations) - end - def ci_projects_union scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } groups = groups_projects.where(members: scope) diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb new file mode 100644 index 00000000000..7d38ac3a374 --- /dev/null +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -0,0 +1,128 @@ +module Users + # Service for refreshing the authorized projects of a user. + # + # This particular service class can not be used to update data for the same + # user concurrently. Doing so could lead to an incorrect state. To ensure this + # doesn't happen a caller must synchronize access (e.g. using + # `Gitlab::ExclusiveLease`). + # + # Usage: + # + # user = User.find_by(username: 'alice') + # service = Users::RefreshAuthorizedProjectsService.new(some_user) + # service.execute + class RefreshAuthorizedProjectsService + attr_reader :user + + LEASE_TIMEOUT = 1.minute.to_i + + # user - The User for which to refresh the authorized projects. + def initialize(user) + @user = user + + # We need an up to date User object that has access to all relations that + # may have been created earlier. The only way to ensure this is to reload + # the User object. + user.reload + end + + # This method returns the updated User object. + def execute + current = current_authorizations_per_project + fresh = fresh_access_levels_per_project + + remove = current.each_with_object([]) do |(project_id, row), array| + # rows not in the new list or with a different access level should be + # removed. + if !fresh[project_id] || fresh[project_id] != row.access_level + array << row.id + end + end + + add = fresh.each_with_object([]) do |(project_id, level), array| + # rows not in the old list or with a different access level should be + # added. + if !current[project_id] || current[project_id].access_level != level + array << [user.id, project_id, level] + end + end + + update_with_lease(remove, add) + end + + # Updates the list of authorizations using an exclusive lease. + def update_with_lease(remove = [], add = []) + lease_key = "refresh_authorized_projects:#{user.id}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. If we don't do so we may end up + # not updating the list of authorized projects properly. To prevent + # hammering Redis too much we'll wait for a bit between retries. + sleep(1) + end + + begin + update_authorizations(remove, add) + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end + end + + # Updates the list of authorizations for the current user. + # + # remove - The IDs of the authorization rows to remove. + # add - Rows to insert in the form `[user id, project id, access level]` + def update_authorizations(remove = [], add = []) + return if remove.empty? && add.empty? + + User.transaction do + user.remove_project_authorizations(remove) unless remove.empty? + ProjectAuthorization.insert_authorizations(add) unless add.empty? + user.set_authorized_projects_column + end + + # Since we batch insert authorization rows, Rails' associations may get + # out of sync. As such we force a reload of the User object. + user.reload + end + + def fresh_access_levels_per_project + fresh_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row.access_level + end + end + + def current_authorizations_per_project + current_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row + end + end + + def current_authorizations + user.project_authorizations.select(:id, :project_id, :access_level) + end + + def fresh_authorizations + ProjectAuthorization. + unscoped. + select('project_id, MAX(access_level) AS access_level'). + from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). + group(:project_id) + end + + private + + # Returns a union query of projects that the user is authorized to access + def project_authorizations_union + relations = [ + user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), + user.groups_projects.select_for_project_authorization, + user.projects.select_for_project_authorization, + user.groups.joins(:shared_projects).select_for_project_authorization + ] + + Gitlab::SQL::Union.new(relations) + end + end +end diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index fccddb70d18..2badd0680fb 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -2,8 +2,6 @@ class AuthorizedProjectsWorker include Sidekiq::Worker include DedicatedSidekiqQueue - LEASE_TIMEOUT = 1.minute.to_i - def self.bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end @@ -11,24 +9,6 @@ class AuthorizedProjectsWorker def perform(user_id) user = User.find_by(id: user_id) - refresh(user) if user - end - - def refresh(user) - lease_key = "refresh_authorized_projects:#{user.id}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. If we don't do so we may end up - # not updating the list of authorized projects properly. To prevent - # hammering Redis too much we'll wait for a bit between retries. - sleep(1) - end - - begin - user.refresh_authorized_projects - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end + user.refresh_authorized_projects if user end end diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb new file mode 100644 index 00000000000..33ef67f97a7 --- /dev/null +++ b/spec/models/project_authorization_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectAuthorization do + let(:user) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } + + describe '.insert_authorizations' do + it 'inserts the authorizations' do + described_class. + insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]]) + + expect(user.project_authorizations.count).to eq(1) + end + + it 'inserts rows in batches' do + described_class.insert_authorizations([ + [user.id, project1.id, Gitlab::Access::MASTER], + [user.id, project2.id, Gitlab::Access::MASTER], + ], 1) + + expect(user.project_authorizations.count).to eq(2) + end + end +end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb new file mode 100644 index 00000000000..72c8f7cd8ec --- /dev/null +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +describe Users::RefreshAuthorizedProjectsService do + let(:project) { create(:empty_project) } + let(:user) { project.namespace.owner } + let(:service) { described_class.new(user) } + + def create_authorization(project, user, access_level = Gitlab::Access::MASTER) + ProjectAuthorization. + create!(project: project, user: user, access_level: access_level) + end + + describe '#execute' do + before do + user.project_authorizations.delete_all + end + + it 'updates the authorized projects of the user' do + project2 = create(:empty_project) + to_remove = create_authorization(project2, user) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'sets the access level of a project to the highest available level' do + to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'returns a User' do + expect(service.execute).to be_an_instance_of(User) + end + end + + describe '#update_with_lease', :redis do + it 'refreshes the authorizations using a lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). + and_return('foo') + + expect(Gitlab::ExclusiveLease).to receive(:cancel). + with(an_instance_of(String), 'foo') + + expect(service).to receive(:update_authorizations).with([1], []) + + service.update_with_lease([1]) + end + end + + describe '#update_authorizations' do + it 'does nothing when there are no rows to add and remove' do + expect(user).not_to receive(:remove_project_authorizations) + expect(ProjectAuthorization).not_to receive(:insert_authorizations) + expect(user).not_to receive(:set_authorized_projects_column) + + service.update_authorizations([], []) + end + + it 'removes authorizations that should be removed' do + authorization = create_authorization(project, user) + + service.update_authorizations([authorization.id]) + + expect(user.project_authorizations).to be_empty + end + + it 'inserts authorizations that should be added' do + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + authorizations = user.project_authorizations + + expect(authorizations.length).to eq(1) + expect(authorizations[0].user_id).to eq(user.id) + expect(authorizations[0].project_id).to eq(project.id) + expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) + end + + it 'populates the authorized projects column' do + # make sure we start with a nil value no matter what the default in the + # factory may be. + user.update(authorized_projects_populated: nil) + + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + expect(user.authorized_projects_populated).to eq(true) + end + end + + describe '#fresh_access_levels_per_project' do + let(:hash) { service.fresh_access_levels_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the access levels' do + expect(hash.values).to eq([Gitlab::Access::MASTER]) + end + end + + describe '#current_authorizations_per_project' do + before { create_authorization(project, user) } + + let(:hash) { service.current_authorizations_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the project authorization rows' do + expect(hash.values).to eq([ProjectAuthorization.first]) + end + end + + describe '#current_authorizations' do + context 'without authorizations' do + it 'returns an empty list' do + expect(service.current_authorizations.empty?).to eq(true) + end + end + + context 'with an authorization' do + before { create_authorization(project, user) } + + let(:row) { service.current_authorizations.take } + + it 'returns the currently authorized projects' do + expect(service.current_authorizations.length).to eq(1) + end + + it 'includes the row ID for every row' do + expect(row.id).to be_a_kind_of(Numeric) + end + + it 'includes the project ID for every row' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level for every row' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end + + describe '#fresh_authorizations' do + it 'returns the new authorized projects' do + expect(service.fresh_authorizations.length).to eq(1) + end + + it 'returns the highest access level' do + project.team.add_guest(user) + + rows = service.fresh_authorizations.to_a + + expect(rows.length).to eq(1) + expect(rows.first.access_level).to eq(Gitlab::Access::MASTER) + end + + context 'every returned row' do + let(:row) { service.fresh_authorizations.take } + + it 'includes the project ID' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end +end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 95e2458da35..b6591f272f6 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do it "refreshes user's authorized projects" do user = create(:user) - expect(worker).to receive(:refresh).with(an_instance_of(User)) + expect_any_instance_of(User).to receive(:refresh_authorized_projects) worker.perform(user.id) end context "when the user is not found" do it "does nothing" do - expect(worker).not_to receive(:refresh) + expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) described_class.new.perform(-1) end end end - - describe '#refresh', redis: true do - it 'refreshes the authorized projects of the user' do - user = create(:user) - - expect(user).to receive(:refresh_authorized_projects) - - worker.refresh(user) - end - end end -- cgit v1.2.1 From e5a8160e85dc72028c6463e77081493c68970868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 11:38:21 +0100 Subject: Add doc for the Gitea importer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- doc/workflow/importing/README.md | 2 +- .../img/import_projects_from_gitea_new_import.png | Bin 0 -> 27805 bytes ...import_projects_from_gitea_new_project_page.png | Bin 0 -> 52976 bytes .../importing/import_projects_from_gitea.md | 80 +++++++++++++++++++++ .../importing/import_projects_from_github.md | 8 +-- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 doc/workflow/importing/img/import_projects_from_gitea_new_import.png create mode 100644 doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png create mode 100644 doc/workflow/importing/import_projects_from_gitea.md diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 18e5d950866..2d91bee0e94 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -4,6 +4,7 @@ 1. [GitHub](import_projects_from_github.md) 1. [GitLab.com](import_projects_from_gitlab_com.md) 1. [FogBugz](import_projects_from_fogbugz.md) +1. [Gitea](import_projects_from_gitea.md) 1. [SVN](migrating_from_svn.md) In addition to the specific migration documentation above, you can import any @@ -14,4 +15,3 @@ repository is too large the import can timeout. You can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. - diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png new file mode 100644 index 00000000000..8d76abf102b Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png differ diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png b/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png new file mode 100644 index 00000000000..91dcf5a6c02 Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png differ diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md new file mode 100644 index 00000000000..f14f8806d8d --- /dev/null +++ b/doc/workflow/importing/import_projects_from_gitea.md @@ -0,0 +1,80 @@ +# Import your project from Gitea to GitLab + +Import your projects from Gitea to GitLab with minimal effort. + +## Overview + +>**Note:** +As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is +a [known issue][issue-401] that should be fixed in a near-future. + +- At its current state, Gitea importer can import: + - the repository description (GitLab 8.15+) + - the Git repository data (GitLab 8.15+) + - the issues (GitLab 8.15+) + - the pull requests (GitLab 8.15+) + - the milestones (GitLab 8.15+) + - the labels (GitLab 8.15+) +- Repository public access is retained. If a repository is private in Gitea + it will be created as private in GitLab as well. + +## How it works + +Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped +to users in your GitLab's instance. This means that the project creator (most of +the times the current user that started the import process) is set as the author, +but a reference on the issue about the original Gitea author is kept. + +The importer will create any new namespaces (groups) if they don't exist or in +the case the namespace is taken, the repository will be imported under the user's +namespace that started the import process. + +## Importing your Gitea repositories + +The importer page is visible when you create a new project. + +![New project page on GitLab](img/import_projects_from_gitea_new_project_page.png) + +Click on the **Gitea** link and the import authorization process will start. + +![New Gitea project import](img/import_projects_from_gitea_new_import.png) + +### Authorize access to your repositories using a personal access token + +With this method, you will perform a one-off authorization with Gitea to grant +GitLab access your repositories: + +1. Go to (replace + `you-gitea-instance` with the host of your Gitea instance). +1. Click **Generate New Token**. +1. Enter a token description. +1. Click **Generate Token**. +1. Copy the token hash. +1. Go back to GitLab and provide the token to the Gitea importer. +1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads + your repositories' information. Once done, you'll be taken to the importer + page to select the repositories to import. + +### Select which repositories to import + +After you've authorized access to your Gitea repositories, you will be +redirected to the Gitea importer page. + +From there, you can see the import statuses of your Gitea repositories. + +- Those that are being imported will show a _started_ status, +- those already successfully imported will be green with a _done_ status, +- whereas those that are not yet imported will have an **Import** button on the + right side of the table. + +If you want, you can import all your Gitea projects in one go by hitting +**Import all projects** in the upper left corner. + +![Gitea importer page](img/import_projects_from_github_importer.png) + +--- + +You can also choose a different name for the project and a different namespace, +if you have the privileges to do so. + +[issue-401]: https://github.com/go-gitea/gitea/issues/401 diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index b3660aa8030..86a016fc6d6 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -6,8 +6,9 @@ Import your projects from GitHub to GitLab with minimal effort. >**Note:** If you are an administrator you can enable the [GitHub integration][gh-import] -in your GitLab instance sitewide. This configuration is optional, users will be -able import their GitHub repositories with a [personal access token][gh-token]. +in your GitLab instance sitewide. This configuration is optional, users will +still be able to import their GitHub repositories with a +[personal access token][gh-token]. - At its current state, GitHub importer can import: - the repository description (GitLab 7.7+) @@ -85,7 +86,7 @@ authorization with GitHub to grant GitLab access your repositories: 1. Click **Generate token**. 1. Copy the token hash. 1. Go back to GitLab and provide the token to the GitHub importer. -1. Hit the **List your GitHub repositories** button and wait while GitLab reads +1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information. Once done, you'll be taken to the importer page to select the repositories to import. @@ -112,7 +113,6 @@ You can also choose a different name for the project and a different namespace, if you have the privileges to do so. [gh-import]: ../../integration/github.md "GitHub integration" -[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" [gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration [gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token [social sign-in]: ../../profile/account/social_sign_in.md -- cgit v1.2.1 From 7f963fcde2cfe577173e91fe08b14ec08ef93dd5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 16 Dec 2016 16:24:08 +0100 Subject: Remove duplicate image and optimize the remaining one --- .../img/import_projects_from_gitea_new_import.png | Bin 27805 -> 15561 bytes .../import_projects_from_gitea_new_project_page.png | Bin 52976 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png index 8d76abf102b..a3f603cbd0a 100644 Binary files a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png and b/doc/workflow/importing/img/import_projects_from_gitea_new_import.png differ diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png b/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png deleted file mode 100644 index 91dcf5a6c02..00000000000 Binary files a/doc/workflow/importing/img/import_projects_from_gitea_new_project_page.png and /dev/null differ -- cgit v1.2.1 From f5d456c87e36829f0a17ecfbbd7b205f2e5ad834 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 16 Dec 2016 16:25:53 +0100 Subject: Fix link to img --- doc/workflow/importing/import_projects_from_gitea.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md index f14f8806d8d..936cee89f45 100644 --- a/doc/workflow/importing/import_projects_from_gitea.md +++ b/doc/workflow/importing/import_projects_from_gitea.md @@ -33,7 +33,7 @@ namespace that started the import process. The importer page is visible when you create a new project. -![New project page on GitLab](img/import_projects_from_gitea_new_project_page.png) +![New project page on GitLab](img/import_projects_from_new_project_page.png) Click on the **Gitea** link and the import authorization process will start. -- cgit v1.2.1 From 5d4531db2555d3051fc47e9268728a670ece95f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Mon, 17 Oct 2016 17:58:57 +0200 Subject: Gogs Importer --- app/assets/images/gogs-logo.svg | 1 + app/controllers/application_controller.rb | 6 +- app/controllers/import/gogs_controller.rb | 76 ++++++++++++++++++++++ app/helpers/import_helper.rb | 8 +++ app/services/projects/import_service.rb | 1 + app/views/import/gogs/new.html.haml | 17 +++++ app/views/import/gogs/status.html.haml | 64 ++++++++++++++++++ app/views/projects/new.html.haml | 5 ++ changelogs/unreleased/22348-gogs-importer.yml | 4 ++ config/initializers/1_settings.rb | 2 +- config/routes/import.rb | 6 ++ lib/gitlab/current_settings.rb | 2 +- lib/gitlab/github_import/client.rb | 14 ++-- lib/gitlab/github_import/importer.rb | 16 ++--- lib/gitlab/github_import/issue_formatter.rb | 2 +- lib/gitlab/github_import/milestone_formatter.rb | 8 ++- lib/gitlab/github_import/project_creator.rb | 9 +-- lib/gitlab/github_import/pull_request_formatter.rb | 2 +- lib/gitlab/gogs_import/importer.rb | 54 +++++++++++++++ lib/gitlab/gogs_import/milestone_formatter.rb | 9 +++ lib/gitlab/import_sources.rb | 1 + 21 files changed, 284 insertions(+), 23 deletions(-) create mode 100644 app/assets/images/gogs-logo.svg create mode 100644 app/controllers/import/gogs_controller.rb create mode 100644 app/views/import/gogs/new.html.haml create mode 100644 app/views/import/gogs/status.html.haml create mode 100644 changelogs/unreleased/22348-gogs-importer.yml create mode 100644 lib/gitlab/gogs_import/importer.rb create mode 100644 lib/gitlab/gogs_import/milestone_formatter.rb diff --git a/app/assets/images/gogs-logo.svg b/app/assets/images/gogs-logo.svg new file mode 100644 index 00000000000..60a18263033 --- /dev/null +++ b/app/assets/images/gogs-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4df80195ae1..3ac4975f815 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :gogs_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('github') end + def gogs_import_enabled? + current_application_settings.import_sources.include?('gogs') + end + def github_import_configured? Gitlab::OAuth::Provider.enabled?(:github) end diff --git a/app/controllers/import/gogs_controller.rb b/app/controllers/import/gogs_controller.rb new file mode 100644 index 00000000000..4caf7d2605a --- /dev/null +++ b/app/controllers/import/gogs_controller.rb @@ -0,0 +1,76 @@ +class Import::GogsController < Import::BaseController + before_action :verify_gogs_import_enabled + before_action :gogs_auth, only: [:status, :jobs, :create] + + rescue_from Octokit::Unauthorized, with: :gogs_unauthorized + + helper_method :logged_in_with_gogs? + + def new + if session[:gogs_access_token] + redirect_to status_import_gogs_url + end + end + + def personal_access_token + session[:gogs_access_token] = params[:personal_access_token] + session[:gogs_host_url] = params[:gogs_host_url] + redirect_to status_import_gogs_url + end + + def status + @repos = client.repos + @already_added_projects = current_user.created_projects.where(import_type: "gogs") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @gogs_root_url = session[:gogs_host_url] + + @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gogs").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id].to_i + repo = client.repo(@repo_id) + @project_name = params[:new_name].presence || repo.name + namespace_path = params[:target_namespace].presence || current_user.namespace_path + @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) + + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: 'gogs').execute + else + render 'unauthorized' + end + end + + private + + def client + @client ||= Gitlab::GithubImport::Client.new(session[:gogs_access_token], host: session[:gogs_host_url], api_version: 'v1') + end + + def verify_gogs_import_enabled + render_404 unless gogs_import_enabled? + end + + def gogs_auth + if session[:gogs_access_token].blank? || session[:gogs_host_url].blank? + redirect_to new_import_gogs_url, + alert: 'You need to specify both an Access Token and a Host URL.' + end + end + + def gogs_unauthorized + session[:gogs_access_token] = nil + redirect_to new_import_gogs_url, + alert: 'Access denied to your Gogs account.' + end + + def access_params + { github_access_token: session[:gogs_access_token] } + end +end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 021d2b14718..29df2703d52 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -8,6 +8,10 @@ module ImportHelper link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' end + def gogs_project_link(path_with_namespace) + link_to path_with_namespace, gogs_project_url(path_with_namespace), target: '_blank' + end + private def github_project_url(path_with_namespace) @@ -20,4 +24,8 @@ module ImportHelper provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } @github_url = provider.fetch('url', 'https://github.com') if provider end + + def gogs_project_url(path_with_namespace) + "#{@gogs_root_url}/#{path_with_namespace}" + end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index d7221fe993c..8cac0b01881 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -5,6 +5,7 @@ module Projects class Error < StandardError; end ALLOWED_TYPES = [ + 'gogs', 'bitbucket', 'fogbugz', 'gitlab', diff --git a/app/views/import/gogs/new.html.haml b/app/views/import/gogs/new.html.haml new file mode 100644 index 00000000000..e1ae1be283c --- /dev/null +++ b/app/views/import/gogs/new.html.haml @@ -0,0 +1,17 @@ +- page_title "Gogs Import" +- header_title "Projects", root_path + +%h3.page-title + = image_tag(image_path('gogs-logo.svg'), alt: 'Gogs', size: "16x16") + Gogs + +%p + To import a Gogs project, you can use a + = succeed '.' do + = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' + += form_tag personal_access_token_import_gogs_path, method: :post, class: 'form-inline' do + .form-group + = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40 + = text_field_tag :gogs_host_url, '', class: 'form-control', placeholder: "Gogs Host URL", size: 128 + = submit_tag 'List Your Gogs Repositories', class: 'btn btn-success' diff --git a/app/views/import/gogs/status.html.haml b/app/views/import/gogs/status.html.haml new file mode 100644 index 00000000000..86ccc79efc8 --- /dev/null +++ b/app/views/import/gogs/status.html.haml @@ -0,0 +1,64 @@ +- page_title "Gogs import" +- header_title "Projects", root_path +%h3.page-title + %i.fa.fa-github + Import projects from Gogs + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th From Gogs + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = gogs_project_link(project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = gogs_project_link(repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gogs_path}", import_path: "#{import_gogs_path}" } } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 0788924d44a..5edb9b69ed2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -48,6 +48,11 @@ - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do = icon('github', text: 'GitHub') + %div + - if gogs_import_enabled? + = link_to new_import_gogs_url, class: 'btn import_gogs' do + = image_tag(image_path('gogs-logo.svg'), alt: 'Gogs', size: "14x14") + Gogs %div - if bitbucket_import_enabled? = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do diff --git a/changelogs/unreleased/22348-gogs-importer.yml b/changelogs/unreleased/22348-gogs-importer.yml new file mode 100644 index 00000000000..9543d2a4d26 --- /dev/null +++ b/changelogs/unreleased/22348-gogs-importer.yml @@ -0,0 +1,4 @@ +--- +title: Gogs importer +merge_request: 6945 +author: Kim Carlbäcker diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ddea325c6ca..c0dbf62bfa2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -213,7 +213,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['domain_whitelist'] ||= [] -Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project] +Settings.gitlab['import_sources'] ||= %w[gogs github bitbucket gitlab google_code fogbugz git gitlab_project] Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) diff --git a/config/routes/import.rb b/config/routes/import.rb index 89f3b3f6378..a0427dade99 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -6,6 +6,12 @@ namespace :import do get :jobs end + resource :gogs, only: [:create, :new], controller: :gogs do + post :personal_access_token + get :status + get :jobs + end + resource :gitlab, only: [:create], controller: :gitlab do get :status get :callback diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index c6bb8f9c8ed..eb3d9f29451 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -45,7 +45,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], domain_whitelist: Settings.gitlab['domain_whitelist'], - import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], + import_sources: %w[gogs github bitbucket gitlab google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 85df6547a67..a96e0bc63dd 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -4,24 +4,30 @@ module Gitlab GITHUB_SAFE_REMAINING_REQUESTS = 100 GITHUB_SAFE_SLEEP_TIME = 500 - attr_reader :access_token + attr_reader :access_token, :host, :api_version - def initialize(access_token) + def initialize(access_token, host: nil, api_version: 'v3') @access_token = access_token + @host = host + @api_version = api_version if access_token ::Octokit.auto_paginate = false end end + def api_endpoint + host.present? && api_version.present? ? "#{host}/api/#{api_version}" : github_options[:site] + end + def api @api ||= ::Octokit::Client.new( access_token: access_token, - api_endpoint: github_options[:site], + api_endpoint: api_endpoint, # If there is no config, we're connecting to github.com and we # should verify ssl. connection_options: { - ssl: { verify: config ? config['verify_ssl'] : true } + ssl: { verify: config ? config['verify_ssl'] : false } } ) end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 281b65bdeba..c32e78cae03 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -60,7 +60,7 @@ module Gitlab fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin - LabelFormatter.new(project, raw).create! + GithubImport::LabelFormatter.new(project, raw).create! rescue => e errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } end @@ -74,7 +74,7 @@ module Gitlab fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| milestones.each do |raw| begin - MilestoneFormatter.new(project, raw).create! + GithubImport::MilestoneFormatter.new(project, raw).create! rescue => e errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } end @@ -85,7 +85,7 @@ module Gitlab def import_issues fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| - gh_issue = IssueFormatter.new(project, raw) + gh_issue = GithubImport::IssueFormatter.new(project, raw) begin issuable = @@ -106,7 +106,7 @@ module Gitlab def import_pull_requests fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| - pull_request = PullRequestFormatter.new(project, raw) + pull_request = GithubImport::PullRequestFormatter.new(project, raw) next unless pull_request.valid? begin @@ -179,7 +179,7 @@ module Gitlab ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) + comment = GithubImport::CommentFormatter.new(project, raw) # GH does not return info about comment's parent, so we guess it by checking its URL! *_, parent, iid = URI(raw.html_url).path.split('/') issuable_class = parent == 'issues' ? Issue : MergeRequest @@ -198,7 +198,7 @@ module Gitlab last_note_attrs = nil cut_off_index = comments.find_index do |raw| - comment = CommentFormatter.new(project, raw) + comment = GithubImport::CommentFormatter.new(project, raw) comment_attrs = comment.attributes last_note_attrs ||= last_note.slice(*comment_attrs.keys) @@ -214,7 +214,7 @@ module Gitlab def import_wiki unless project.wiki.repository_exists? - wiki = WikiFormatter.new(project) + wiki = GithubImport::WikiFormatter.new(project) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) end rescue Gitlab::Shell::Error => e @@ -230,7 +230,7 @@ module Gitlab fetch_resources(:releases, repo, per_page: 100) do |releases| releases.each do |raw| begin - gh_release = ReleaseFormatter.new(project, raw) + gh_release = GithubImport::ReleaseFormatter.new(project, raw) gh_release.create! if gh_release.valid? rescue => e errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 887690bcc7c..21a3dee203b 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -70,7 +70,7 @@ module Gitlab def milestone if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.number) + project.milestones.find_by(iid: raw_data.milestone.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr)) end end diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index 401dd962521..678d56b830b 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -3,7 +3,7 @@ module Gitlab class MilestoneFormatter < BaseFormatter def attributes { - iid: raw_data.number, + iid: raw_data.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr), project: project, title: raw_data.title, description: raw_data.description, @@ -19,7 +19,11 @@ module Gitlab end def find_condition - { iid: raw_data.number } + { iid: raw_data.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr) } + end + + def self.iid_attr + :number end private diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index a2410068845..3f635be22ba 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -1,14 +1,15 @@ module Gitlab module GithubImport class ProjectCreator - attr_reader :repo, :name, :namespace, :current_user, :session_data + attr_reader :repo, :name, :namespace, :current_user, :session_data, :type - def initialize(repo, name, namespace, current_user, session_data) + def initialize(repo, name, namespace, current_user, session_data, type: 'github') @repo = repo @name = name @namespace = namespace @current_user = current_user @session_data = session_data + @type = type end def execute @@ -19,7 +20,7 @@ module Gitlab description: repo.description, namespace_id: namespace.id, visibility_level: visibility_level, - import_type: "github", + import_type: type, import_source: repo.full_name, import_url: import_url, skip_wiki: skip_wiki @@ -29,7 +30,7 @@ module Gitlab private def import_url - repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@") + repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@") end def visibility_level diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index b9a227fb11a..ea8768fded7 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -98,7 +98,7 @@ module Gitlab def milestone if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.number) + project.milestones.find_by(iid: raw_data.milestone.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr)) end end diff --git a/lib/gitlab/gogs_import/importer.rb b/lib/gitlab/gogs_import/importer.rb new file mode 100644 index 00000000000..604e31d35a3 --- /dev/null +++ b/lib/gitlab/gogs_import/importer.rb @@ -0,0 +1,54 @@ +require 'uri' + +module Gitlab + module GogsImport + class Importer < Gitlab::GithubImport::Importer + include Gitlab::ShellAdapter + + attr_reader :client, :errors, :project, :repo, :repo_url + + def initialize(project) + @project = project + @repo = project.import_source + @repo_url = project.import_url + @errors = [] + @labels = {} + + if credentials + uri = URI.parse(project.import_url) + host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(/[\w-]+\/[\w-]+\.git\z/, '') + @client = GithubImport::Client.new(credentials[:user], host: host, api_version: 'v1') + else + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + end + end + + def execute + import_labels + import_milestones + import_pull_requests + import_issues + import_comments(:issues) + import_comments(:pull_requests) + import_wiki + # NOTE: this is commented out since Gogs doesn't have release-API yet + # import_releases + handle_errors + + true + end + + def import_milestones + fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| + milestones.each do |raw| + begin + GogsImport::MilestoneFormatter.new(project, raw).create! + rescue => e + errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end + end + end + end + end + end +end diff --git a/lib/gitlab/gogs_import/milestone_formatter.rb b/lib/gitlab/gogs_import/milestone_formatter.rb new file mode 100644 index 00000000000..990e792929a --- /dev/null +++ b/lib/gitlab/gogs_import/milestone_formatter.rb @@ -0,0 +1,9 @@ +module Gitlab + module GogsImport + class MilestoneFormatter < GithubImport::MilestoneFormatter + def self.iid_attr + :id + end + end + end +end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 94261b7eeed..9564c4cc134 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -14,6 +14,7 @@ module Gitlab def options { + 'Gogs' => 'gogs', 'GitHub' => 'github', 'Bitbucket' => 'bitbucket', 'GitLab.com' => 'gitlab', -- cgit v1.2.1 From 103114e3d73819f76bed9d8ad1bbdb8964875579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 15 Dec 2016 17:31:14 +0100 Subject: Rename Gogs to Gitea, DRY the controller and improve views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/assets/images/gogs-logo.svg | 1 - app/controllers/application_controller.rb | 6 +- app/controllers/import/gitea_controller.rb | 41 ++++ app/controllers/import/github_controller.rb | 91 +++++--- app/controllers/import/gogs_controller.rb | 76 ------- app/helpers/import_helper.rb | 8 +- app/services/projects/import_service.rb | 27 ++- app/views/import/gitea/new.html.haml | 23 +++ app/views/import/gitea/status.html.haml | 64 ++++++ app/views/import/gogs/new.html.haml | 17 -- app/views/import/gogs/status.html.haml | 64 ------ app/views/projects/new.html.haml | 10 +- app/views/shared/icons/_go_logo.svg.erb | 1 + changelogs/unreleased/22348-gitea-importer.yml | 4 + changelogs/unreleased/22348-gogs-importer.yml | 4 - config/initializers/1_settings.rb | 2 +- config/routes/import.rb | 2 +- lib/gitlab/current_settings.rb | 2 +- lib/gitlab/gogs_import/importer.rb | 54 ----- lib/gitlab/gogs_import/milestone_formatter.rb | 9 - lib/gitlab/import_sources.rb | 4 +- spec/controllers/import/gitea_controller_spec.rb | 43 ++++ spec/controllers/import/github_controller_spec.rb | 218 +------------------- spec/routing/import_routing_spec.rb | 166 +++++++++++++++ .../githubish_import_controller_shared_context.rb | 10 + .../githubish_import_controller_shared_examples.rb | 228 +++++++++++++++++++++ spec/support/import_spec_helper.rb | 4 + 27 files changed, 689 insertions(+), 490 deletions(-) delete mode 100644 app/assets/images/gogs-logo.svg create mode 100644 app/controllers/import/gitea_controller.rb delete mode 100644 app/controllers/import/gogs_controller.rb create mode 100644 app/views/import/gitea/new.html.haml create mode 100644 app/views/import/gitea/status.html.haml delete mode 100644 app/views/import/gogs/new.html.haml delete mode 100644 app/views/import/gogs/status.html.haml create mode 100644 app/views/shared/icons/_go_logo.svg.erb create mode 100644 changelogs/unreleased/22348-gitea-importer.yml delete mode 100644 changelogs/unreleased/22348-gogs-importer.yml delete mode 100644 lib/gitlab/gogs_import/importer.rb delete mode 100644 lib/gitlab/gogs_import/milestone_formatter.rb create mode 100644 spec/controllers/import/gitea_controller_spec.rb create mode 100644 spec/routing/import_routing_spec.rb create mode 100644 spec/support/githubish_import_controller_shared_context.rb create mode 100644 spec/support/githubish_import_controller_shared_examples.rb diff --git a/app/assets/images/gogs-logo.svg b/app/assets/images/gogs-logo.svg deleted file mode 100644 index 60a18263033..00000000000 --- a/app/assets/images/gogs-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3ac4975f815..bb47e2a8bf7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :gogs_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -245,8 +245,8 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('github') end - def gogs_import_enabled? - current_application_settings.import_sources.include?('gogs') + def gitea_import_enabled? + current_application_settings.import_sources.include?('gitea') end def github_import_configured? diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb new file mode 100644 index 00000000000..c82a20be04c --- /dev/null +++ b/app/controllers/import/gitea_controller.rb @@ -0,0 +1,41 @@ +class Import::GiteaController < Import::GithubController + def new + if session[:access_token].present? && session[:host_url].present? + redirect_to status_import_url + end + end + + def personal_access_token + session[:host_url] = params[:gitea_host_url] + super + end + + def status + @gitea_root_url = session[:host_url] + super + end + + private + + # Overriden methods + def provider + :gitea + end + + # Gitea is not yet an OAuth provider + # See https://github.com/go-gitea/gitea/issues/27 + def logged_in_with_provider? + false + end + + def provider_auth + if session[:access_token].blank? || session[:host_url].blank? + redirect_to new_import_gitea_url, + alert: 'You need to specify both an Access Token and a Host URL.' + end + end + + def client_options + { host: session[:host_url], api_version: 'v1' } + end +end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index ee7d498c59c..343ca51e510 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,39 +1,37 @@ class Import::GithubController < Import::BaseController - before_action :verify_github_import_enabled - before_action :github_auth, only: [:status, :jobs, :create] + before_action :verify_import_enabled + before_action :provider_auth, only: [:status, :jobs, :create] - rescue_from Octokit::Unauthorized, with: :github_unauthorized - - helper_method :logged_in_with_github? + rescue_from Octokit::Unauthorized, with: :provider_unauthorized def new - if logged_in_with_github? - go_to_github_for_permissions - elsif session[:github_access_token] - redirect_to status_import_github_url + if logged_in_with_provider? + go_to_provider_for_permissions + elsif session[:access_token] + redirect_to status_import_url end end def callback - session[:github_access_token] = client.get_token(params[:code]) - redirect_to status_import_github_url + session[:access_token] = client.get_token(params[:code]) + redirect_to status_import_url end def personal_access_token - session[:github_access_token] = params[:personal_access_token] - redirect_to status_import_github_url + session[:access_token] = params[:personal_access_token] + redirect_to status_import_url end def status @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: "github") + @already_added_projects = current_user.created_projects.where(import_type: provider) already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs - jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status]) + jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) render json: jobs end @@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController namespace_path = params[:target_namespace].presence || current_user.namespace_path @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute + if can?(current_user, :create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute else render 'unauthorized' end @@ -54,34 +52,59 @@ class Import::GithubController < Import::BaseController private def client - @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) + @client ||= Gitlab::GithubImport::Client.new(session[:access_token], client_options) end - def verify_github_import_enabled - render_404 unless github_import_enabled? + def verify_import_enabled + render_404 unless import_enabled? end - def github_auth - if session[:github_access_token].blank? - go_to_github_for_permissions - end + def go_to_provider_for_permissions + redirect_to client.authorize_url(callback_import_url) + end + + def import_enabled? + __send__("#{provider}_import_enabled?") end - def go_to_github_for_permissions - redirect_to client.authorize_url(callback_import_github_url) + def new_import_url + public_send("new_import_#{provider}_url") end - def github_unauthorized - session[:github_access_token] = nil - redirect_to new_import_github_url, - alert: 'Access denied to your GitHub account.' + def status_import_url + public_send("status_import_#{provider}_url") end - def logged_in_with_github? - current_user.identities.exists?(provider: 'github') + def callback_import_url + public_send("callback_import_#{provider}_url") + end + + def provider_unauthorized + session[:access_token] = nil + redirect_to new_import_url, + alert: "Access denied to your #{Gitlab::ImportSources.options.key(provider.to_s)} account." end def access_params - { github_access_token: session[:github_access_token] } + { github_access_token: session[:access_token] } + end + + # The following methods are overriden in subclasses + def provider + :github + end + + def logged_in_with_provider? + current_user.identities.exists?(provider: provider) + end + + def provider_auth + if session[:access_token].blank? + go_to_provider_for_permissions + end + end + + def client_options + {} end end diff --git a/app/controllers/import/gogs_controller.rb b/app/controllers/import/gogs_controller.rb deleted file mode 100644 index 4caf7d2605a..00000000000 --- a/app/controllers/import/gogs_controller.rb +++ /dev/null @@ -1,76 +0,0 @@ -class Import::GogsController < Import::BaseController - before_action :verify_gogs_import_enabled - before_action :gogs_auth, only: [:status, :jobs, :create] - - rescue_from Octokit::Unauthorized, with: :gogs_unauthorized - - helper_method :logged_in_with_gogs? - - def new - if session[:gogs_access_token] - redirect_to status_import_gogs_url - end - end - - def personal_access_token - session[:gogs_access_token] = params[:personal_access_token] - session[:gogs_host_url] = params[:gogs_host_url] - redirect_to status_import_gogs_url - end - - def status - @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: "gogs") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @gogs_root_url = session[:gogs_host_url] - - @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } - end - - def jobs - jobs = current_user.created_projects.where(import_type: "gogs").to_json(only: [:id, :import_status]) - render json: jobs - end - - def create - @repo_id = params[:repo_id].to_i - repo = client.repo(@repo_id) - @project_name = params[:new_name].presence || repo.name - namespace_path = params[:target_namespace].presence || current_user.namespace_path - @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: 'gogs').execute - else - render 'unauthorized' - end - end - - private - - def client - @client ||= Gitlab::GithubImport::Client.new(session[:gogs_access_token], host: session[:gogs_host_url], api_version: 'v1') - end - - def verify_gogs_import_enabled - render_404 unless gogs_import_enabled? - end - - def gogs_auth - if session[:gogs_access_token].blank? || session[:gogs_host_url].blank? - redirect_to new_import_gogs_url, - alert: 'You need to specify both an Access Token and a Host URL.' - end - end - - def gogs_unauthorized - session[:gogs_access_token] = nil - redirect_to new_import_gogs_url, - alert: 'Access denied to your Gogs account.' - end - - def access_params - { github_access_token: session[:gogs_access_token] } - end -end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 29df2703d52..fb79e2a4eef 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -8,8 +8,8 @@ module ImportHelper link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' end - def gogs_project_link(path_with_namespace) - link_to path_with_namespace, gogs_project_url(path_with_namespace), target: '_blank' + def gitea_project_link(root_url, path_with_namespace) + link_to path_with_namespace, gitea_project_url(root_url, path_with_namespace), target: '_blank' end private @@ -25,7 +25,7 @@ module ImportHelper @github_url = provider.fetch('url', 'https://github.com') if provider end - def gogs_project_url(path_with_namespace) - "#{@gogs_root_url}/#{path_with_namespace}" + def gitea_project_url(root_url, path_with_namespace) + "#{root_url}/#{path_with_namespace}" end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 8cac0b01881..287c0a4257f 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -5,13 +5,13 @@ module Projects class Error < StandardError; end ALLOWED_TYPES = [ - 'gogs', + 'github', 'bitbucket', - 'fogbugz', 'gitlab', - 'github', 'google_code', - 'gitlab_project' + 'fogbugz', + 'gitlab_project', + 'gitea' ] def execute @@ -71,8 +71,23 @@ module Projects def importer return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? - class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" - class_name.constantize.new(project) + class_name = + case project.import_type + when 'github', 'gitea' + Gitlab::GithubImport::Importer + when 'bitbucket' + Gitlab::BitbucketImport::Importer + when 'gitlab' + Gitlab::GitlabImport::Importer + when 'google_code' + Gitlab::GoogleCodeImport::Importer + when 'fogbugz' + Gitlab::FogbugzImport::Importer + else + raise 'Unknown importer type!' + end + + class_name.new(project) end def unknown_url? diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml new file mode 100644 index 00000000000..02a116f996b --- /dev/null +++ b/app/views/import/gitea/new.html.haml @@ -0,0 +1,23 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path + +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + +%p + To get started, please enter your Gitea Host URL and a + = succeed '.' do + = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' + += form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do + .form-group + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label' + .col-sm-4 + = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' + .form-group + = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label' + .col-sm-4 + = text_field_tag :personal_access_token, nil, class: 'form-control' + .form-actions + = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create' diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml new file mode 100644 index 00000000000..2b25892c0da --- /dev/null +++ b/app/views/import/gitea/status.html.haml @@ -0,0 +1,64 @@ +- page_title "Gitea import" +- header_title "Projects", root_path +%h3.page-title + = custom_icon('go_logo') + Import projects from Gitea + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th From Gitea + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = gitea_project_link(@gitea_root_url, project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = gitea_project_link(@gitea_root_url, repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitea_path}", import_path: "#{import_gitea_path}" } } diff --git a/app/views/import/gogs/new.html.haml b/app/views/import/gogs/new.html.haml deleted file mode 100644 index e1ae1be283c..00000000000 --- a/app/views/import/gogs/new.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- page_title "Gogs Import" -- header_title "Projects", root_path - -%h3.page-title - = image_tag(image_path('gogs-logo.svg'), alt: 'Gogs', size: "16x16") - Gogs - -%p - To import a Gogs project, you can use a - = succeed '.' do - = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' - -= form_tag personal_access_token_import_gogs_path, method: :post, class: 'form-inline' do - .form-group - = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40 - = text_field_tag :gogs_host_url, '', class: 'form-control', placeholder: "Gogs Host URL", size: 128 - = submit_tag 'List Your Gogs Repositories', class: 'btn btn-success' diff --git a/app/views/import/gogs/status.html.haml b/app/views/import/gogs/status.html.haml deleted file mode 100644 index 86ccc79efc8..00000000000 --- a/app/views/import/gogs/status.html.haml +++ /dev/null @@ -1,64 +0,0 @@ -- page_title "Gogs import" -- header_title "Projects", root_path -%h3.page-title - %i.fa.fa-github - Import projects from Gogs - -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From Gogs - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = gogs_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = gogs_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gogs_path}", import_path: "#{import_gogs_path}" } } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 5edb9b69ed2..866b278ce57 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -48,11 +48,6 @@ - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do = icon('github', text: 'GitHub') - %div - - if gogs_import_enabled? - = link_to new_import_gogs_url, class: 'btn import_gogs' do - = image_tag(image_path('gogs-logo.svg'), alt: 'Gogs', size: "14x14") - Gogs %div - if bitbucket_import_enabled? = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do @@ -73,6 +68,11 @@ - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb new file mode 100644 index 00000000000..5052651c110 --- /dev/null +++ b/app/views/shared/icons/_go_logo.svg.erb @@ -0,0 +1 @@ + diff --git a/changelogs/unreleased/22348-gitea-importer.yml b/changelogs/unreleased/22348-gitea-importer.yml new file mode 100644 index 00000000000..ce81a3cfefb --- /dev/null +++ b/changelogs/unreleased/22348-gitea-importer.yml @@ -0,0 +1,4 @@ +--- +title: New Gitea importer +merge_request: 6945 +author: diff --git a/changelogs/unreleased/22348-gogs-importer.yml b/changelogs/unreleased/22348-gogs-importer.yml deleted file mode 100644 index 9543d2a4d26..00000000000 --- a/changelogs/unreleased/22348-gogs-importer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Gogs importer -merge_request: 6945 -author: Kim Carlbäcker diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index c0dbf62bfa2..ee97b4e42b9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -213,7 +213,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['domain_whitelist'] ||= [] -Settings.gitlab['import_sources'] ||= %w[gogs github bitbucket gitlab google_code fogbugz git gitlab_project] +Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea] Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) diff --git a/config/routes/import.rb b/config/routes/import.rb index a0427dade99..c378253bf15 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -6,7 +6,7 @@ namespace :import do get :jobs end - resource :gogs, only: [:create, :new], controller: :gogs do + resource :gitea, only: [:create, :new], controller: :gitea do post :personal_access_token get :status get :jobs diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index eb3d9f29451..9d142f1b82e 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -45,7 +45,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], domain_whitelist: Settings.gitlab['domain_whitelist'], - import_sources: %w[gogs github bitbucket gitlab google_code fogbugz git gitlab_project], + import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/gogs_import/importer.rb b/lib/gitlab/gogs_import/importer.rb deleted file mode 100644 index 604e31d35a3..00000000000 --- a/lib/gitlab/gogs_import/importer.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'uri' - -module Gitlab - module GogsImport - class Importer < Gitlab::GithubImport::Importer - include Gitlab::ShellAdapter - - attr_reader :client, :errors, :project, :repo, :repo_url - - def initialize(project) - @project = project - @repo = project.import_source - @repo_url = project.import_url - @errors = [] - @labels = {} - - if credentials - uri = URI.parse(project.import_url) - host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(/[\w-]+\/[\w-]+\.git\z/, '') - @client = GithubImport::Client.new(credentials[:user], host: host, api_version: 'v1') - else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" - end - end - - def execute - import_labels - import_milestones - import_pull_requests - import_issues - import_comments(:issues) - import_comments(:pull_requests) - import_wiki - # NOTE: this is commented out since Gogs doesn't have release-API yet - # import_releases - handle_errors - - true - end - - def import_milestones - fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| - milestones.each do |raw| - begin - GogsImport::MilestoneFormatter.new(project, raw).create! - rescue => e - errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } - end - end - end - end - end - end -end diff --git a/lib/gitlab/gogs_import/milestone_formatter.rb b/lib/gitlab/gogs_import/milestone_formatter.rb deleted file mode 100644 index 990e792929a..00000000000 --- a/lib/gitlab/gogs_import/milestone_formatter.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module GogsImport - class MilestoneFormatter < GithubImport::MilestoneFormatter - def self.iid_attr - :id - end - end - end -end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 9564c4cc134..34587582bd1 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -14,14 +14,14 @@ module Gitlab def options { - 'Gogs' => 'gogs', 'GitHub' => 'github', 'Bitbucket' => 'bitbucket', 'GitLab.com' => 'gitlab', 'Google Code' => 'google_code', 'FogBugz' => 'fogbugz', 'Repo by URL' => 'git', - 'GitLab export' => 'gitlab_project' + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea' } end end diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb new file mode 100644 index 00000000000..3064d1dd58a --- /dev/null +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Import::GiteaController do + include ImportSpecHelper + + let(:provider) { :gitea } + let(:host_url) { 'https://try.gitea.io' } + + include_context 'a GitHub-ish import controller' + + def assign_host_url + session[:host_url] = host_url + end + + describe "GET new" do + it_behaves_like 'a GitHub-ish import controller: GET new' do + before do + assign_host_url + end + end + end + + describe "POST personal_access_token" do + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' + end + + describe "GET status" do + it_behaves_like 'a GitHub-ish import controller: GET status' do + before do + assign_host_url + end + let(:extra_assign_expectations) { { gitea_root_url: host_url } } + end + end + + describe 'POST create' do + it_behaves_like 'a GitHub-ish import controller: POST create' do + before do + assign_host_url + end + end + end +end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 4f96567192d..55820a7cc65 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -3,34 +3,18 @@ require 'spec_helper' describe Import::GithubController do include ImportSpecHelper - let(:user) { create(:user) } - let(:token) { "asdasd12345" } - let(:access_params) { { github_access_token: token } } + let(:provider) { :github } - def assign_session_token - session[:github_access_token] = token - end - - before do - sign_in(user) - allow(controller).to receive(:github_import_enabled?).and_return(true) - end + include_context 'a GitHub-ish import controller' describe "GET new" do - it "redirects to GitHub for an access token if logged in with GitHub" do - allow(controller).to receive(:logged_in_with_github?).and_return(true) - expect(controller).to receive(:go_to_github_for_permissions) + it_behaves_like 'a GitHub-ish import controller: GET new' - get :new - end - - it "redirects to status if we already have a token" do - assign_session_token - allow(controller).to receive(:logged_in_with_github?).and_return(false) + it "redirects to GitHub for an access token if logged in with GitHub" do + allow(controller).to receive(:logged_in_with_provider?).and_return(true) + expect(controller).to receive(:go_to_provider_for_permissions) get :new - - expect(controller).to redirect_to(status_import_github_url) end end @@ -45,202 +29,20 @@ describe Import::GithubController do get :callback - expect(session[:github_access_token]).to eq(token) + expect(session[:access_token]).to eq(token) expect(controller).to redirect_to(status_import_github_url) end end describe "POST personal_access_token" do - it "updates access token" do - token = "asdfasdf9876" - - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:user).and_return(true) - - post :personal_access_token, personal_access_token: token - - expect(session[:github_access_token]).to eq(token) - expect(controller).to redirect_to(status_import_github_url) - end + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' end describe "GET status" do - before do - @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') - @org = OpenStruct.new(login: 'company') - @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') - assign_session_token - end - - it "assigns variables" do - @project = create(:project, import_type: 'github', creator_id: user.id) - stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo, @org_repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [@repo], orgs: []) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end - - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:repos).and_raise(Octokit::Unauthorized) - - get :status - - expect(session[:github_access_token]).to eq(nil) - expect(controller).to redirect_to(new_import_github_url) - expect(flash[:alert]).to eq('Access denied to your GitHub account.') - end + it_behaves_like 'a GitHub-ish import controller: GET status' end describe "POST create" do - let(:github_username) { user.username } - let(:github_user) { OpenStruct.new(login: github_username) } - let(:github_repo) do - OpenStruct.new( - name: 'vim', - full_name: "#{github_username}/vim", - owner: OpenStruct.new(login: github_username) - ) - end - - before do - stub_client(user: github_user, repo: github_repo) - assign_session_token - end - - context "when the repository owner is the GitHub user" do - context "when the GitHub user and GitLab user's usernames match" do - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the GitHub user and GitLab user's usernames don't match" do - let(:github_username) { "someone_else" } - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when the repository owner is not the GitHub user" do - let(:other_username) { "someone_else" } - - before do - github_repo.owner = OpenStruct.new(login: other_username) - assign_session_token - end - - context "when a namespace with the GitHub user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } - - context "when the namespace is owned by the GitLab user" do - it "takes the existing namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - - it "creates a project using user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when a namespace with the GitHub user's username doesn't exist" do - context "when current user can create namespaces" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1) - end - - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) - - post :create, target_namespace: github_repo.name, format: :js - end - end - - context "when current user can't create namespaces" do - before do - user.update_attribute(:can_create_group, false) - end - - it "doesn't create the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, format: :js }.not_to change(Namespace, :count) - end - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context 'user has chosen a namespace and name for the project' do - let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } - let(:test_name) { 'test_name' } - - it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, test_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } - end - - it 'takes the selected name and default namespace' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { new_name: test_name, format: :js } - end - end - end + it_behaves_like 'a GitHub-ish import controller: POST create' end end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb new file mode 100644 index 00000000000..f1234ff470b --- /dev/null +++ b/spec/routing/import_routing_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' + +# Shared examples for a resource inside a Project +# +# By default it tests all the default REST actions: index, create, new, edit, +# show, update, and destroy. You can remove actions by customizing the +# `actions` variable. +# +# It also expects a `controller` variable to be available which defines both +# the path to the resource as well as the controller name. +# +# Examples +# +# # Default behavior +# it_behaves_like 'RESTful project resources' do +# let(:controller) { 'issues' } +# end +# +# # Customizing actions +# it_behaves_like 'RESTful project resources' do +# let(:actions) { [:index] } +# let(:controller) { 'issues' } +# end +shared_examples 'importer routing' do + let(:except_actions) { [] } + + it 'to #create' do + expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) + end + + it 'to #new' do + expect(get("/import/#{provider}/new")).to route_to("import/#{provider}#new") unless except_actions.include?(:new) + end + + it 'to #status' do + expect(get("/import/#{provider}/status")).to route_to("import/#{provider}#status") unless except_actions.include?(:status) + end + + it 'to #callback' do + expect(get("/import/#{provider}/callback")).to route_to("import/#{provider}#callback") unless except_actions.include?(:callback) + end + + it 'to #jobs' do + expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) + end +end + +# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token +# status_import_github GET /import/github/status(.:format) import/github#status +# callback_import_github GET /import/github/callback(.:format) import/github#callback +# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# import_github POST /import/github(.:format) import/github#create +# new_import_github GET /import/github/new(.:format) import/github#new +describe Import::GithubController, 'routing' do + it_behaves_like 'importer routing' do + let(:provider) { 'github' } + end + + it 'to #personal_access_token' do + expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token') + end +end + +# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token +# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status +# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# import_gitea POST /import/gitea(.:format) import/gitea#create +# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new +describe Import::GiteaController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'gitea' } + end + + it 'to #personal_access_token' do + expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token') + end + +end + +# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status +# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback +# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs +# import_gitlab POST /import/gitlab(.:format) import/gitlab#create +describe Import::GitlabController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'gitlab' } + end +end + +# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status +# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback +# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs +# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create +describe Import::BitbucketController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'bitbucket' } + end +end + +# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status +# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback +# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs +# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map +# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map +# import_google_code POST /import/google_code(.:format) import/google_code#create +# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new +describe Import::GoogleCodeController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'google_code' } + end + + it 'to #callback' do + expect(post("/import/google_code/callback")).to route_to("import/google_code#callback") + end + + it 'to #new_user_map' do + expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map') + end +end + +# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status +# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback +# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs +# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map +# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map +# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create +# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new +describe Import::FogbugzController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'fogbugz' } + end + + it 'to #callback' do + expect(post("/import/fogbugz/callback")).to route_to("import/fogbugz#callback") + end + + it 'to #new_user_map' do + expect(get('/import/fogbugz/user_map')).to route_to('import/fogbugz#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/fogbugz/user_map')).to route_to('import/fogbugz#create_user_map') + end +end + +# import_gitlab_project POST /import/gitlab_project(.:format) import/gitlab_projects#create +# POST /import/gitlab_project(.:format) import/gitlab_projects#create +# new_import_gitlab_project GET /import/gitlab_project/new(.:format) import/gitlab_projects#new +describe Import::GitlabProjectsController, 'routing' do + it 'to #create' do + expect(post('/import/gitlab_project')).to route_to('import/gitlab_projects#create') + end + + it 'to #new' do + expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new') + end +end diff --git a/spec/support/githubish_import_controller_shared_context.rb b/spec/support/githubish_import_controller_shared_context.rb new file mode 100644 index 00000000000..e71994edec6 --- /dev/null +++ b/spec/support/githubish_import_controller_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'a GitHub-ish import controller' do + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { github_access_token: token } } + + before do + sign_in(user) + allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true) + end +end diff --git a/spec/support/githubish_import_controller_shared_examples.rb b/spec/support/githubish_import_controller_shared_examples.rb new file mode 100644 index 00000000000..aa2d8aed0bd --- /dev/null +++ b/spec/support/githubish_import_controller_shared_examples.rb @@ -0,0 +1,228 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +shared_examples 'a GitHub-ish import controller: POST personal_access_token' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "updates access token" do + token = 'asdfasdf9876' + + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:user).and_return(true) + + post :personal_access_token, personal_access_token: token + + expect(session[:access_token]).to eq(token) + expect(controller).to redirect_to(status_import_url) + end +end + +shared_examples 'a GitHub-ish import controller: GET new' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "redirects to status if we already have a token" do + assign_session_token + allow(controller).to receive(:logged_in_with_provider?).and_return(false) + + get :new + + expect(controller).to redirect_to(status_import_url) + end + + it "renders the :new page if no token is present in session" do + get :new + + expect(response).to render_template(:new) + end +end + +shared_examples 'a GitHub-ish import controller: GET status' do + let(:new_import_url) { public_send("new_import_#{provider}_url") } + let(:user) { create(:user) } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:org) { OpenStruct.new(login: 'company') } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } + let(:extra_assign_expectations) { {} } + + before do + assign_session_token + end + + it "assigns variables" do + project = create(:empty_project, import_type: provider, creator_id: user.id) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([repo, org_repo]) + extra_assign_expectations.each do |key, value| + expect(assigns(key)).to eq(value) + end + end + + it "does not show already added project" do + project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [repo], orgs: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([]) + end + + it "handles an invalid access token" do + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:repos).and_raise(Octokit::Unauthorized) + + get :status + + expect(session[:access_token]).to eq(nil) + expect(controller).to redirect_to(new_import_url) + expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.options.key(provider.to_s)} account.") + end +end + +shared_examples 'a GitHub-ish import controller: POST create' do + let(:user) { create(:user) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + stub_client(user: provider_user, repo: provider_repo) + assign_session_token + end + + context "when the repository owner is the Gitea user" do + context "when the Gitea user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the Gitea user and GitLab user's usernames don't match" do + let(:provider_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the Gitea user" do + let(:other_username) { "someone_else" } + + before do + provider_repo.owner = OpenStruct.new(login: other_username) + assign_session_token + end + + context "when a namespace with the Gitea user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "creates a project using user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when a namespace with the Gitea user's username doesn't exist" do + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, target_namespace: provider_repo.name, format: :js + end + end + + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end + + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context 'user has chosen a namespace and name for the project' do + let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } + let(:test_name) { 'test_name' } + + it 'takes the selected namespace and name' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } + end + + it 'takes the selected name and default namespace' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { new_name: test_name, format: :js } + end + end + end +end diff --git a/spec/support/import_spec_helper.rb b/spec/support/import_spec_helper.rb index 6710962f082..cd25e05ac4b 100644 --- a/spec/support/import_spec_helper.rb +++ b/spec/support/import_spec_helper.rb @@ -30,4 +30,8 @@ module ImportSpecHelper ) allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end + + def assign_session_token + session[:access_token] = 'asdasd12345' + end end -- cgit v1.2.1 From 99ddd1dcbed35b642d7bd8a52cc6e5e5453b9f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 15 Dec 2016 17:36:53 +0100 Subject: Modify GithubImport to support Gitea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reason is that Gitea plan to be GitHub-compatible so it makes sense to just modify GitHubImport a bit for now, and hopefully we can change it to GitHubishImport once Gitea is 100%-compatible. Signed-off-by: Rémy Coutable --- app/controllers/import/gitea_controller.rb | 2 +- app/helpers/import_helper.rb | 8 +- app/views/import/gitea/status.html.haml | 8 +- app/views/import/github/status.html.haml | 5 +- changelogs/unreleased/22348-gitea-importer.yml | 2 +- lib/gitlab/github_import/base_formatter.rb | 4 + lib/gitlab/github_import/client.rb | 16 +- lib/gitlab/github_import/importer.rb | 80 +++-- lib/gitlab/github_import/issuable_formatter.rb | 60 ++++ lib/gitlab/github_import/issue_formatter.rb | 52 +-- lib/gitlab/github_import/milestone_formatter.rb | 12 +- lib/gitlab/github_import/pull_request_formatter.rb | 60 +--- spec/controllers/import/gitea_controller_spec.rb | 2 +- spec/lib/gitlab/github_import/client_spec.rb | 46 ++- spec/lib/gitlab/github_import/importer_spec.rb | 370 +++++++++++++-------- .../github_import/issuable_formatter_spec.rb | 21 ++ .../gitlab/github_import/issue_formatter_spec.rb | 38 ++- .../github_import/milestone_formatter_spec.rb | 27 +- .../github_import/pull_request_formatter_spec.rb | 34 +- spec/routing/import_routing_spec.rb | 1 - .../githubish_import_controller_shared_context.rb | 10 + .../githubish_import_controller_shared_examples.rb | 228 +++++++++++++ .../githubish_import_controller_shared_context.rb | 10 - .../githubish_import_controller_shared_examples.rb | 228 ------------- 24 files changed, 750 insertions(+), 574 deletions(-) create mode 100644 lib/gitlab/github_import/issuable_formatter.rb create mode 100644 spec/lib/gitlab/github_import/issuable_formatter_spec.rb create mode 100644 spec/support/controllers/githubish_import_controller_shared_context.rb create mode 100644 spec/support/controllers/githubish_import_controller_shared_examples.rb delete mode 100644 spec/support/githubish_import_controller_shared_context.rb delete mode 100644 spec/support/githubish_import_controller_shared_examples.rb diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index c82a20be04c..3bc21e62a1e 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -11,7 +11,7 @@ class Import::GiteaController < Import::GithubController end def status - @gitea_root_url = session[:host_url] + @gitea_host_url = session[:host_url] super end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index fb79e2a4eef..f52a0f176e9 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -8,8 +8,8 @@ module ImportHelper link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' end - def gitea_project_link(root_url, path_with_namespace) - link_to path_with_namespace, gitea_project_url(root_url, path_with_namespace), target: '_blank' + def gitea_project_link(path_with_namespace) + link_to path_with_namespace, gitea_project_url(path_with_namespace), target: '_blank' end private @@ -25,7 +25,7 @@ module ImportHelper @github_url = provider.fetch('url', 'https://github.com') if provider end - def gitea_project_url(root_url, path_with_namespace) - "#{root_url}/#{path_with_namespace}" + def gitea_project_url(path_with_namespace) + "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" end end diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml index 2b25892c0da..a7321632994 100644 --- a/app/views/import/gitea/status.html.haml +++ b/app/views/import/gitea/status.html.haml @@ -1,8 +1,8 @@ -- page_title "Gitea import" +- page_title "Gitea Import" - header_title "Projects", root_path %h3.page-title = custom_icon('go_logo') - Import projects from Gitea + Import Projects from Gitea %p.light Select projects you want to import. @@ -26,7 +26,7 @@ - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} %td - = gitea_project_link(@gitea_root_url, project.import_source) + = gitea_project_link(project.import_source) %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -43,7 +43,7 @@ - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td - = gitea_project_link(@gitea_root_url, repo.full_name) + = gitea_project_link(repo.full_name) %td.import-target %fieldset.row .input-group diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 4c721d40b55..70b18ee217d 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,8 +1,7 @@ -- page_title "GitHub import" +- page_title "GitHub Import" - header_title "Projects", root_path %h3.page-title - %i.fa.fa-github - Import projects from GitHub + = icon 'github', text: 'Import Projects from GitHub' %p.light Select projects you want to import. diff --git a/changelogs/unreleased/22348-gitea-importer.yml b/changelogs/unreleased/22348-gitea-importer.yml index ce81a3cfefb..2aeefb0b259 100644 --- a/changelogs/unreleased/22348-gitea-importer.yml +++ b/changelogs/unreleased/22348-gitea-importer.yml @@ -1,4 +1,4 @@ --- title: New Gitea importer -merge_request: 6945 +merge_request: 8116 author: diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 6dbae64a9fe..95dba9a327b 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -15,6 +15,10 @@ module Gitlab end end + def url + raw_data.url || '' + end + private def gitlab_user_id(github_id) diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index a96e0bc63dd..ba869faa92e 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -8,7 +8,7 @@ module Gitlab def initialize(access_token, host: nil, api_version: 'v3') @access_token = access_token - @host = host + @host = host.to_s.sub(%r{/+\z}, '') @api_version = api_version if access_token @@ -16,10 +16,6 @@ module Gitlab end end - def api_endpoint - host.present? && api_version.present? ? "#{host}/api/#{api_version}" : github_options[:site] - end - def api @api ||= ::Octokit::Client.new( access_token: access_token, @@ -27,7 +23,7 @@ module Gitlab # If there is no config, we're connecting to github.com and we # should verify ssl. connection_options: { - ssl: { verify: config ? config['verify_ssl'] : false } + ssl: { verify: config ? config['verify_ssl'] : true } } ) end @@ -70,6 +66,14 @@ module Gitlab private + def api_endpoint + if host.present? && api_version.present? + "#{host}/api/#{api_version}" + else + github_options[:site] + end + end + def config Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" } end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index c32e78cae03..c53cd1a928d 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,7 +3,7 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :client, :errors, :project, :repo, :repo_url + attr_reader :errors, :project, :repo, :repo_url def initialize(project) @project = project @@ -11,12 +11,27 @@ module Gitlab @repo_url = project.import_url @errors = [] @labels = {} + end + + def client + return @client if defined?(@client) + unless credentials + raise Projects::ImportService::Error, + "Unable to find project import data credentials for project ID: #{@project.id}" + end - if credentials - @client = Client.new(credentials[:user]) - else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + opts = {} + # Gitea plan to be GitHub compliant + if project.import_type == 'gitea' + uri = URI.parse(project.import_url) + host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '') + opts = { + host: host, + api_version: 'v1' + } end + + @client = Client.new(credentials[:user], opts) end def execute @@ -35,7 +50,13 @@ module Gitlab import_comments(:issues) import_comments(:pull_requests) import_wiki - import_releases + + # Gitea doesn't have a Release API yet + # See https://github.com/go-gitea/gitea/issues/330 + unless project.import_type == 'gitea' + import_releases + end + handle_errors true @@ -44,7 +65,9 @@ module Gitlab private def credentials - @credentials ||= project.import_data.credentials if project.import_data + return @credentials if defined?(@credentials) + + @credentials = project.import_data ? project.import_data.credentials : nil end def handle_errors @@ -60,9 +83,10 @@ module Gitlab fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin - GithubImport::LabelFormatter.new(project, raw).create! + gh_label = LabelFormatter.new(project, raw) + gh_label.create! rescue => e - errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message } end end end @@ -74,9 +98,10 @@ module Gitlab fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| milestones.each do |raw| begin - GithubImport::MilestoneFormatter.new(project, raw).create! + gh_milestone = MilestoneFormatter.new(project, raw) + gh_milestone.create! rescue => e - errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message } end end end @@ -85,7 +110,7 @@ module Gitlab def import_issues fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| - gh_issue = GithubImport::IssueFormatter.new(project, raw) + gh_issue = IssueFormatter.new(project, raw) begin issuable = @@ -97,7 +122,7 @@ module Gitlab apply_labels(issuable, raw) rescue => e - errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message } end end end @@ -106,18 +131,23 @@ module Gitlab def import_pull_requests fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| - pull_request = GithubImport::PullRequestFormatter.new(project, raw) - next unless pull_request.valid? + gh_pull_request = PullRequestFormatter.new(project, raw) + next unless gh_pull_request.valid? begin - restore_source_branch(pull_request) unless pull_request.source_branch_exists? - restore_target_branch(pull_request) unless pull_request.target_branch_exists? + restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists? + restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists? + + merge_request = gh_pull_request.create! - pull_request.create! + # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage + if project.import_type == 'gitea' + apply_labels(merge_request, raw) + end rescue => e - errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } + errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message } ensure - clean_up_restored_branches(pull_request) + clean_up_restored_branches(gh_pull_request) end end end @@ -179,7 +209,7 @@ module Gitlab ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = GithubImport::CommentFormatter.new(project, raw) + comment = CommentFormatter.new(project, raw) # GH does not return info about comment's parent, so we guess it by checking its URL! *_, parent, iid = URI(raw.html_url).path.split('/') issuable_class = parent == 'issues' ? Issue : MergeRequest @@ -198,7 +228,7 @@ module Gitlab last_note_attrs = nil cut_off_index = comments.find_index do |raw| - comment = GithubImport::CommentFormatter.new(project, raw) + comment = CommentFormatter.new(project, raw) comment_attrs = comment.attributes last_note_attrs ||= last_note.slice(*comment_attrs.keys) @@ -214,7 +244,7 @@ module Gitlab def import_wiki unless project.wiki.repository_exists? - wiki = GithubImport::WikiFormatter.new(project) + wiki = WikiFormatter.new(project) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) end rescue Gitlab::Shell::Error => e @@ -230,10 +260,10 @@ module Gitlab fetch_resources(:releases, repo, per_page: 100) do |releases| releases.each do |raw| begin - gh_release = GithubImport::ReleaseFormatter.new(project, raw) + gh_release = ReleaseFormatter.new(project, raw) gh_release.create! if gh_release.valid? rescue => e - errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message } end end end diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb new file mode 100644 index 00000000000..256f360efc7 --- /dev/null +++ b/lib/gitlab/github_import/issuable_formatter.rb @@ -0,0 +1,60 @@ +module Gitlab + module GithubImport + class IssuableFormatter < BaseFormatter + def project_association + raise NotImplementedError + end + + def number + raw_data.number + end + + def find_condition + { iid: number } + end + + private + + def state + raw_data.state == 'closed' ? 'closed' : 'opened' + end + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gitlab_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gitlab_author_id || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + if gitlab_author_id + body + else + formatter.author_line(author) + body + end + end + + def milestone + if raw_data.milestone.present? + milestone = MilestoneFormatter.new(project, raw_data.milestone) + project.milestones.find_by(milestone.find_condition) + end + end + end + end +end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 21a3dee203b..6f5ac4dac0d 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -1,6 +1,6 @@ module Gitlab module GithubImport - class IssueFormatter < BaseFormatter + class IssueFormatter < IssuableFormatter def attributes { iid: number, @@ -24,59 +24,9 @@ module Gitlab :issues end - def find_condition - { iid: number } - end - - def number - raw_data.number - end - def pull_request? raw_data.pull_request.present? end - - private - - def assigned? - raw_data.assignee.present? - end - - def assignee_id - if assigned? - gitlab_user_id(raw_data.assignee.id) - end - end - - def author - raw_data.user.login - end - - def author_id - gitlab_author_id || project.creator_id - end - - def body - raw_data.body || "" - end - - def description - if gitlab_author_id - body - else - formatter.author_line(author) + body - end - end - - def milestone - if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr)) - end - end - - def state - raw_data.state == 'closed' ? 'closed' : 'opened' - end end end end diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index 678d56b830b..fe172b854d5 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -3,7 +3,7 @@ module Gitlab class MilestoneFormatter < BaseFormatter def attributes { - iid: raw_data.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr), + iid: number, project: project, title: raw_data.title, description: raw_data.description, @@ -19,11 +19,15 @@ module Gitlab end def find_condition - { iid: raw_data.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr) } + { iid: number } end - def self.iid_attr - :number + def number + if project.import_type == 'gitea' + raw_data.id + else + raw_data.number + end end private diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index ea8768fded7..4ea0200e89b 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,6 +1,6 @@ module Gitlab module GithubImport - class PullRequestFormatter < BaseFormatter + class PullRequestFormatter < IssuableFormatter delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true @@ -28,14 +28,6 @@ module Gitlab :merge_requests end - def find_condition - { iid: number } - end - - def number - raw_data.number - end - def valid? source_branch.valid? && target_branch.valid? end @@ -60,57 +52,15 @@ module Gitlab end end - def url - raw_data.url - end - private - def assigned? - raw_data.assignee.present? - end - - def assignee_id - if assigned? - gitlab_user_id(raw_data.assignee.id) - end - end - - def author - raw_data.user.login - end - - def author_id - gitlab_author_id || project.creator_id - end - - def body - raw_data.body || "" - end - - def description - if gitlab_author_id - body + def state + if raw_data.state == 'closed' && raw_data.merged_at.present? + 'merged' else - formatter.author_line(author) + body + super end end - - def milestone - if raw_data.milestone.present? - project.milestones.find_by(iid: raw_data.milestone.public_send("Gitlab::#{project.import_type.camelize}Import::MilestoneFormatter".constantize.iid_attr)) - end - end - - def state - @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present? - 'merged' - elsif raw_data.state == 'closed' - 'closed' - else - 'opened' - end - end end end end diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 3064d1dd58a..3643386ffbc 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -29,7 +29,7 @@ describe Import::GiteaController do before do assign_host_url end - let(:extra_assign_expectations) { { gitea_root_url: host_url } } + let(:extra_assign_expectations) { { gitea_host_url: host_url } } end end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index e829b936343..21f2a9e225b 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do end end - context 'when provider does not specity an API endpoint' do - it 'uses GitHub root API endpoint' do - expect(client.api.api_endpoint).to eq 'https://api.github.com/' + describe '#api_endpoint' do + context 'when provider does not specity an API endpoint' do + it 'uses GitHub root API endpoint' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end end - end - context 'when provider specify a custom API endpoint' do - before do - github_provider['args']['client_options']['site'] = 'https://github.company.com/' + context 'when provider specify a custom API endpoint' do + before do + github_provider['args']['client_options']['site'] = 'https://github.company.com/' + end + + it 'uses the custom API endpoint' do + expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) + expect(client.api.api_endpoint).to eq 'https://github.company.com/' + end + end + + context 'when given a host' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') } + + it 'builds a endpoint with the given host and the default API version' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end - it 'uses the custom API endpoint' do - expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) - expect(client.api.api_endpoint).to eq 'https://github.company.com/' + context 'when given an API version' do + subject(:client) { described_class.new(token, api_version: 'v3') } + + it 'does not use the API version without a host' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end + end + + context 'when given a host and version' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') } + + it 'builds a endpoint with the given options' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 9e027839f59..0a03b7353f6 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -1,169 +1,251 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do - describe '#execute' do + shared_examples 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [] } + before do - allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + allow(project).to receive(:import_data).and_return(double.as_null_object) end - context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } - let(:octocat) { double(id: 123456, login: 'octocat') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - - let(:label1) do - double( - name: 'Bug', - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + it 'calls import methods' do + importer = described_class.new(project) - let(:label2) do - double( - name: nil, - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + expected_called = [ + :import_labels, :import_milestones, :import_pull_requests, :import_issues, + :import_wiki, :import_releases, :handle_errors + ] - let(:milestone) do - double( - number: 1347, - state: 'open', - title: '1.0', - description: 'Version 1.0', - due_on: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1' - ) - end + expected_called -= expected_not_called - let(:issue1) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'Found a bug', - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347', - labels: [double(name: 'Label #1')], - ) - end + aggregate_failures do + expected_called.each do |method_name| + expect(importer).to receive(method_name) + end - let(:issue2) do - double( - number: 1348, - milestone: nil, - state: 'open', - title: nil, - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348', - labels: [double(name: 'Label #2')], - ) - end + expect(importer).to receive(:import_comments).with(:issues) + expect(importer).to receive(:import_comments).with(:pull_requests) - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347', - ) + expected_not_called.each do |method_name| + expect(importer).not_to receive(method_name) + end end - let(:release1) do - double( - tag_name: 'v1.0.0', - name: 'First release', - body: 'Release v1.0.0', - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/1' - ) - end + importer.execute + end + end - let(:release2) do - double( - tag_name: 'v2.0.0', - name: 'Second release', - body: nil, - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/2' - ) - end + shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do + before do + allow(project).to receive(:import_data).and_return(double.as_null_object) - before do - allow(project).to receive(:import_data).and_return(double.as_null_object) - allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) - allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) - allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) - allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) - allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) - allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) - allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) - end + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + + allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) + allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) + allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) + end + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:label1) do + double( + name: 'Bug', + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:milestone) do + double( + id: 1347, # For Gitea + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/milestones/1" + ) + end - it 'returns true' do - expect(described_class.new(project).execute).to eq true + let(:issue1) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1347", + labels: [double(name: 'Label #1')] + ) + end + + let(:issue2) do + double( + number: 1348, + milestone: nil, + state: 'open', + title: nil, + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1348", + labels: [double(name: 'Label #2')] + ) + end + + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + + let(:release1) do + double( + tag_name: 'v1.0.0', + name: 'First release', + body: 'Release v1.0.0', + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/1" + ) + end + + let(:release2) do + double( + tag_name: 'v2.0.0', + name: 'Second release', + body: nil, + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/2" + ) + end + + it 'returns true' do + expect(described_class.new(project).execute).to eq true + end + + it 'does not raise an error' do + expect { described_class.new(project).execute }.not_to raise_error + end + + it 'stores error messages' do + error = { + message: 'The remote data could not be fully imported.', + errors: [ + { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, + { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, + { type: :wiki, errors: "Gitlab::Shell::Error" } + ] + } + + unless project.import_type == 'gitea' + error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" } end - it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error + described_class.new(project).execute + + expect(project.import_error).to eq error.to_json + end + end + + let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:credentials) { { user: 'joe' } } + + context 'when importing a GitHub project' do + let(:api_root) { 'https://api.github.com' } + let(:repo_root) { 'https://github.com' } + + it_behaves_like 'Gitlab::GithubImport::Importer#execute' + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + {} + ) + + described_class.new(project).client end + end + end - it 'stores error messages' do - error = { - message: 'The remote data could not be fully imported.', - errors: [ - { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, - { type: :wiki, errors: "Gitlab::Shell::Error" }, - { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } - ] - } + context 'when importing a Gitea project' do + let(:api_root) { 'https://try.gitea.io/api/v1' } + let(:repo_root) { 'https://try.gitea.io' } + before do + project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") + end - described_class.new(project).execute + it_behaves_like 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [:import_releases] } + end + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + { host: "#{repo_root}:443/foo", api_version: 'v1' } + ) - expect(project.import_error).to eq error.to_json + described_class.new(project).client end end end diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb new file mode 100644 index 00000000000..6bc5f98ed2c --- /dev/null +++ b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssuableFormatter, lib: true do + let(:raw_data) do + double(number: 42) + end + let(:project) { double(import_type: 'github') } + let(:issuable_formatter) { described_class.new(project, raw_data) } + + describe '#project_association' do + it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) } + end + + describe '#number' do + it { expect(issuable_formatter.number).to eq(42) } + end + + describe '#find_condition' do + it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) } + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 95339e2f128..e31ed9c1fa0 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data)} + subject(:issue) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(issue.attributes.fetch(:milestone)).to eq milestone end @@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end + shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do + let(:raw_data) { double(base_data.merge(number: 1347)) } + + it 'returns issue number' do + expect(issue.number).to eq 1347 + end + end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + describe '#has_comments?' do context 'when number of comments is greater than zero' do let(:raw_data) { double(base_data.merge(comments: 1)) } @@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } - - it 'returns pull request number' do - expect(issue.number).to eq 1347 - end - end - describe '#pull_request?' do context 'when mention a pull request' do let(:raw_data) { double(base_data.merge(pull_request: double)) } diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 09337c99a07..6d38041c468 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { - number: 1347, state: 'open', title: '1.0', description: 'Version 1.0', @@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do closed_at: nil } end + let(:iid_attr) { :number } - subject(:formatter) { described_class.new(project, raw_data)} + subject(:formatter) { described_class.new(project, raw_data) } + + shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do + let(:data) { base_data.merge(iid_attr => 1347) } - describe '#attributes' do context 'when milestone is open' do - let(:raw_data) { double(base_data.merge(state: 'open')) } + let(:raw_data) { double(data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { @@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:raw_data) { double(base_data.merge(state: 'closed')) } + let(:raw_data) { double(data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do context 'when milestone has a due date' do let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(due_on: due_date)) } + let(:raw_data) { double(data.merge(due_on: due_date)) } it 'returns formatted attributes' do expected = { @@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end end end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end + + context 'when importing a Gitea project' do + let(:iid_attr) { :id } + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 302f0fc0623..2b3256edcb2 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data)} + subject(:pull_request) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(pull_request.attributes.fetch(:milestone)).to eq milestone end end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do + let(:raw_data) { double(base_data) } it 'returns pull request number' do expect(pull_request.number).to eq 1347 end end - describe '#source_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#target_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb index f1234ff470b..78ff9c6e6fd 100644 --- a/spec/routing/import_routing_spec.rb +++ b/spec/routing/import_routing_spec.rb @@ -75,7 +75,6 @@ describe Import::GiteaController, 'routing' do it 'to #personal_access_token' do expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token') end - end # status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status diff --git a/spec/support/controllers/githubish_import_controller_shared_context.rb b/spec/support/controllers/githubish_import_controller_shared_context.rb new file mode 100644 index 00000000000..e71994edec6 --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'a GitHub-ish import controller' do + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { github_access_token: token } } + + before do + sign_in(user) + allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true) + end +end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb new file mode 100644 index 00000000000..e11ab802095 --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -0,0 +1,228 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +shared_examples 'a GitHub-ish import controller: POST personal_access_token' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "updates access token" do + token = 'asdfasdf9876' + + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:user).and_return(true) + + post :personal_access_token, personal_access_token: token + + expect(session[:access_token]).to eq(token) + expect(controller).to redirect_to(status_import_url) + end +end + +shared_examples 'a GitHub-ish import controller: GET new' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "redirects to status if we already have a token" do + assign_session_token + allow(controller).to receive(:logged_in_with_provider?).and_return(false) + + get :new + + expect(controller).to redirect_to(status_import_url) + end + + it "renders the :new page if no token is present in session" do + get :new + + expect(response).to render_template(:new) + end +end + +shared_examples 'a GitHub-ish import controller: GET status' do + let(:new_import_url) { public_send("new_import_#{provider}_url") } + let(:user) { create(:user) } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:org) { OpenStruct.new(login: 'company') } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } + let(:extra_assign_expectations) { {} } + + before do + assign_session_token + end + + it "assigns variables" do + project = create(:empty_project, import_type: provider, creator_id: user.id) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([repo, org_repo]) + extra_assign_expectations.each do |key, value| + expect(assigns(key)).to eq(value) + end + end + + it "does not show already added project" do + project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [repo], orgs: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([]) + end + + it "handles an invalid access token" do + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:repos).and_raise(Octokit::Unauthorized) + + get :status + + expect(session[:access_token]).to eq(nil) + expect(controller).to redirect_to(new_import_url) + expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") + end +end + +shared_examples 'a GitHub-ish import controller: POST create' do + let(:user) { create(:user) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + stub_client(user: provider_user, repo: provider_repo) + assign_session_token + end + + context "when the repository owner is the Gitea user" do + context "when the Gitea user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the Gitea user and GitLab user's usernames don't match" do + let(:provider_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the Gitea user" do + let(:other_username) { "someone_else" } + + before do + provider_repo.owner = OpenStruct.new(login: other_username) + assign_session_token + end + + context "when a namespace with the Gitea user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "creates a project using user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when a namespace with the Gitea user's username doesn't exist" do + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, target_namespace: provider_repo.name, format: :js + end + end + + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end + + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context 'user has chosen a namespace and name for the project' do + let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } + let(:test_name) { 'test_name' } + + it 'takes the selected namespace and name' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } + end + + it 'takes the selected name and default namespace' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { new_name: test_name, format: :js } + end + end + end +end diff --git a/spec/support/githubish_import_controller_shared_context.rb b/spec/support/githubish_import_controller_shared_context.rb deleted file mode 100644 index e71994edec6..00000000000 --- a/spec/support/githubish_import_controller_shared_context.rb +++ /dev/null @@ -1,10 +0,0 @@ -shared_context 'a GitHub-ish import controller' do - let(:user) { create(:user) } - let(:token) { "asdasd12345" } - let(:access_params) { { github_access_token: token } } - - before do - sign_in(user) - allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true) - end -end diff --git a/spec/support/githubish_import_controller_shared_examples.rb b/spec/support/githubish_import_controller_shared_examples.rb deleted file mode 100644 index aa2d8aed0bd..00000000000 --- a/spec/support/githubish_import_controller_shared_examples.rb +++ /dev/null @@ -1,228 +0,0 @@ -# Specifications for behavior common to all objects with an email attribute. -# Takes a list of email-format attributes and requires: -# - subject { "the object with a attribute= setter" } -# Note: You have access to `email_value` which is the email address value -# being currently tested). - -shared_examples 'a GitHub-ish import controller: POST personal_access_token' do - let(:status_import_url) { public_send("status_import_#{provider}_url") } - - it "updates access token" do - token = 'asdfasdf9876' - - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:user).and_return(true) - - post :personal_access_token, personal_access_token: token - - expect(session[:access_token]).to eq(token) - expect(controller).to redirect_to(status_import_url) - end -end - -shared_examples 'a GitHub-ish import controller: GET new' do - let(:status_import_url) { public_send("status_import_#{provider}_url") } - - it "redirects to status if we already have a token" do - assign_session_token - allow(controller).to receive(:logged_in_with_provider?).and_return(false) - - get :new - - expect(controller).to redirect_to(status_import_url) - end - - it "renders the :new page if no token is present in session" do - get :new - - expect(response).to render_template(:new) - end -end - -shared_examples 'a GitHub-ish import controller: GET status' do - let(:new_import_url) { public_send("new_import_#{provider}_url") } - let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } - let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } - let(:extra_assign_expectations) { {} } - - before do - assign_session_token - end - - it "assigns variables" do - project = create(:empty_project, import_type: provider, creator_id: user.id) - stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([repo, org_repo]) - extra_assign_expectations.each do |key, value| - expect(assigns(key)).to eq(value) - end - end - - it "does not show already added project" do - project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [repo], orgs: []) - - get :status - - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([]) - end - - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:repos).and_raise(Octokit::Unauthorized) - - get :status - - expect(session[:access_token]).to eq(nil) - expect(controller).to redirect_to(new_import_url) - expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.options.key(provider.to_s)} account.") - end -end - -shared_examples 'a GitHub-ish import controller: POST create' do - let(:user) { create(:user) } - let(:provider_username) { user.username } - let(:provider_user) { OpenStruct.new(login: provider_username) } - let(:provider_repo) do - OpenStruct.new( - name: 'vim', - full_name: "#{provider_username}/vim", - owner: OpenStruct.new(login: provider_username) - ) - end - - before do - stub_client(user: provider_user, repo: provider_repo) - assign_session_token - end - - context "when the repository owner is the Gitea user" do - context "when the Gitea user and GitLab user's usernames match" do - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the Gitea user and GitLab user's usernames don't match" do - let(:provider_username) { "someone_else" } - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when the repository owner is not the Gitea user" do - let(:other_username) { "someone_else" } - - before do - provider_repo.owner = OpenStruct.new(login: other_username) - assign_session_token - end - - context "when a namespace with the Gitea user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } - - context "when the namespace is owned by the GitLab user" do - it "takes the existing namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - - it "creates a project using user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when a namespace with the Gitea user's username doesn't exist" do - context "when current user can create namespaces" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) - end - - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, target_namespace: provider_repo.name, format: :js - end - end - - context "when current user can't create namespaces" do - before do - user.update_attribute(:can_create_group, false) - end - - it "doesn't create the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, format: :js }.not_to change(Namespace, :count) - end - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context 'user has chosen a namespace and name for the project' do - let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } - let(:test_name) { 'test_name' } - - it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } - end - - it 'takes the selected name and default namespace' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) - - post :create, { new_name: test_name, format: :js } - end - end - end -end -- cgit v1.2.1 From 8fc63d1f648fa38eac9e5422dd42667d8e7f1b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 09:15:30 +0100 Subject: Improve Gitlab::ImportSources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/import/github_controller.rb | 2 +- app/services/projects/import_service.rb | 32 +--------- lib/gitlab/import_sources.rb | 40 ++++++++---- spec/lib/gitlab/import_sources_spec.rb | 94 +++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 spec/lib/gitlab/import_sources_spec.rb diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 343ca51e510..4ae121ec482 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -82,7 +82,7 @@ class Import::GithubController < Import::BaseController def provider_unauthorized session[:access_token] = nil redirect_to new_import_url, - alert: "Access denied to your #{Gitlab::ImportSources.options.key(provider.to_s)} account." + alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." end def access_params diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 287c0a4257f..cd230528743 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -4,16 +4,6 @@ module Projects class Error < StandardError; end - ALLOWED_TYPES = [ - 'github', - 'bitbucket', - 'gitlab', - 'google_code', - 'fogbugz', - 'gitlab_project', - 'gitea' - ] - def execute add_repository_to_project unless project.gitlab_project_import? @@ -65,29 +55,11 @@ module Projects end def has_importer? - ALLOWED_TYPES.include?(project.import_type) + Gitlab::ImportSources.importer_names.include?(project.import_type) end def importer - return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? - - class_name = - case project.import_type - when 'github', 'gitea' - Gitlab::GithubImport::Importer - when 'bitbucket' - Gitlab::BitbucketImport::Importer - when 'gitlab' - Gitlab::GitlabImport::Importer - when 'google_code' - Gitlab::GoogleCodeImport::Importer - when 'fogbugz' - Gitlab::FogbugzImport::Importer - else - raise 'Unknown importer type!' - end - - class_name.new(project) + Gitlab::ImportSources.importer(project.import_type).new(project) end def unknown_url? diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 34587582bd1..45958710c13 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -7,22 +7,38 @@ module Gitlab module ImportSources extend CurrentSettings + ImportSource = Struct.new(:name, :title, :importer) + + ImportTable = [ + ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer), + ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer), + ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer), + ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer), + ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer), + ImportSource.new('git', 'Repo by URL', nil), + ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer), + ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer) + ].freeze + class << self + def options + @options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }] + end + def values - options.values + @values ||= ImportTable.map(&:name) end - def options - { - 'GitHub' => 'github', - 'Bitbucket' => 'bitbucket', - 'GitLab.com' => 'gitlab', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', - 'Repo by URL' => 'git', - 'GitLab export' => 'gitlab_project', - 'Gitea' => 'gitea' - } + def importer_names + @importer_names ||= ImportTable.select(&:importer).map(&:name) + end + + def importer(name) + ImportTable.find { |import_source| import_source.name == name }.importer + end + + def title(name) + options.key(name) end end end diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb new file mode 100644 index 00000000000..8cea38e9ff8 --- /dev/null +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::ImportSources do + describe '.options' do + it 'returns a hash' do + expected = + { + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea' + } + + expect(described_class.options).to eq(expected) + end + end + + describe '.values' do + it 'returns an array' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'git', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.values).to eq(expected) + end + end + + describe '.importer_names' do + it 'returns an array of importer names' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.importer_names).to eq(expected) + end + end + + describe '.importer' do + import_sources = { + 'github' => Gitlab::GithubImport::Importer, + 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'gitlab' => Gitlab::GitlabImport::Importer, + 'google_code' => Gitlab::GoogleCodeImport::Importer, + 'fogbugz' => Gitlab::FogbugzImport::Importer, + 'git' => nil, + 'gitlab_project' => Gitlab::ImportExport::Importer, + 'gitea' => Gitlab::GithubImport::Importer + } + + import_sources.each do |name, klass| + it "returns #{klass} when given #{name}" do + expect(described_class.importer(name)).to eq(klass) + end + end + end + + describe '.title' do + import_sources = { + 'github' => 'GitHub', + 'bitbucket' => 'Bitbucket', + 'gitlab' => 'GitLab.com', + 'google_code' => 'Google Code', + 'fogbugz' => 'FogBugz', + 'git' => 'Repo by URL', + 'gitlab_project' => 'GitLab export', + 'gitea' => 'Gitea' + } + + import_sources.each do |name, title| + it "returns #{title} when given #{name}" do + expect(described_class.title(name)).to eq(title) + end + end + end +end -- cgit v1.2.1 From 20aff5cd2b782fa47fe6c15aad07a547179ee147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 10:47:26 +0100 Subject: Reduce duplication for GitHubish import status view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/helpers/import_helper.rb | 8 ++-- app/views/import/_githubish_status.html.haml | 61 ++++++++++++++++++++++++++++ app/views/import/gitea/status.html.haml | 59 +-------------------------- app/views/import/github/status.html.haml | 59 +-------------------------- spec/helpers/import_helper_spec.rb | 33 ++++++++++----- 5 files changed, 89 insertions(+), 131 deletions(-) create mode 100644 app/views/import/_githubish_status.html.haml diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index f52a0f176e9..a0642a1894b 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -4,12 +4,10 @@ module ImportHelper "#{namespace}/#{name}" end - def github_project_link(path_with_namespace) - link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' - end + def provider_project_link(provider, path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) - def gitea_project_link(path_with_namespace) - link_to path_with_namespace, gitea_project_url(path_with_namespace), target: '_blank' + link_to path_with_namespace, url, target: '_blank' end private diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml new file mode 100644 index 00000000000..f12f9482a51 --- /dev/null +++ b/app/views/import/_githubish_status.html.haml @@ -0,0 +1,61 @@ +- provider = local_assigns.fetch(:provider) +- provider_title = Gitlab::ImportSources.title(provider) + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= "From #{provider_title}" + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = provider_project_link(provider, project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = provider_project_link(provider, repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } } diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml index a7321632994..589ca27e45d 100644 --- a/app/views/import/gitea/status.html.haml +++ b/app/views/import/gitea/status.html.haml @@ -4,61 +4,4 @@ = custom_icon('go_logo') Import Projects from Gitea -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From Gitea - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = gitea_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = gitea_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitea_path}", import_path: "#{import_gitea_path}" } } += render 'import/githubish_status', provider: 'gitea' diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 70b18ee217d..0fe578a0036 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -3,61 +3,4 @@ %h3.page-title = icon 'github', text: 'Import Projects from GitHub' -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From GitHub - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = github_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = github_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } += render 'import/githubish_status', provider: 'github' diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 187b891b927..10f293cddf5 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -25,24 +25,37 @@ describe ImportHelper do end end - describe '#github_project_link' do - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + describe '#provider_project_link' do + context 'when provider is "github"' do + context 'when provider does not specify a custom URL' do + it 'uses default GitHub URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.com/octocat/Hello-World"') + end end - end - context 'when provider specify a custom URL' do - it 'uses custom URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + context 'when provider specify a custom URL' do + it 'uses custom URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.company.com/octocat/Hello-World"') + end + end + end + + context 'when provider is "gitea"' do + before do + assign(:gitea_host_url, 'https://try.gitea.io/') + end + + it 'uses given host' do + expect(helper.provider_project_link('gitea', 'octocat/Hello-World')). + to include('href="https://try.gitea.io/octocat/Hello-World"') end end end -- cgit v1.2.1 From e046e4c14d06a19cc30a679f4943c77b56ee6d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 17:43:34 +0100 Subject: Namespace access token session key in `Import::GithubController` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/import/gitea_controller.rb | 14 +++++++---- app/controllers/import/github_controller.rb | 18 ++++++++------ spec/controllers/import/gitea_controller_spec.rb | 2 +- spec/controllers/import/github_controller_spec.rb | 2 +- .../githubish_import_controller_shared_examples.rb | 28 ++++++++++++---------- spec/support/import_spec_helper.rb | 4 ---- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 3bc21e62a1e..fbd851c64a7 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -1,22 +1,26 @@ class Import::GiteaController < Import::GithubController def new - if session[:access_token].present? && session[:host_url].present? + if session[access_token_key].present? && session[host_key].present? redirect_to status_import_url end end def personal_access_token - session[:host_url] = params[:gitea_host_url] + session[host_key] = params[host_key] super end def status - @gitea_host_url = session[:host_url] + @gitea_host_url = session[host_key] super end private + def host_key + :"#{provider}_host_url" + end + # Overriden methods def provider :gitea @@ -29,13 +33,13 @@ class Import::GiteaController < Import::GithubController end def provider_auth - if session[:access_token].blank? || session[:host_url].blank? + if session[access_token_key].blank? || session[host_key].blank? redirect_to new_import_gitea_url, alert: 'You need to specify both an Access Token and a Host URL.' end end def client_options - { host: session[:host_url], api_version: 'v1' } + { host: session[host_key], api_version: 'v1' } end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 4ae121ec482..53a5981e564 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -7,18 +7,18 @@ class Import::GithubController < Import::BaseController def new if logged_in_with_provider? go_to_provider_for_permissions - elsif session[:access_token] + elsif session[access_token_key] redirect_to status_import_url end end def callback - session[:access_token] = client.get_token(params[:code]) + session[access_token_key] = client.get_token(params[:code]) redirect_to status_import_url end def personal_access_token - session[:access_token] = params[:personal_access_token] + session[access_token_key] = params[:personal_access_token] redirect_to status_import_url end @@ -52,7 +52,7 @@ class Import::GithubController < Import::BaseController private def client - @client ||= Gitlab::GithubImport::Client.new(session[:access_token], client_options) + @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options) end def verify_import_enabled @@ -80,13 +80,17 @@ class Import::GithubController < Import::BaseController end def provider_unauthorized - session[:access_token] = nil + session[access_token_key] = nil redirect_to new_import_url, alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." end + def access_token_key + :"#{provider}_access_token" + end + def access_params - { github_access_token: session[:access_token] } + { github_access_token: session[access_token_key] } end # The following methods are overriden in subclasses @@ -99,7 +103,7 @@ class Import::GithubController < Import::BaseController end def provider_auth - if session[:access_token].blank? + if session[access_token_key].blank? go_to_provider_for_permissions end end diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 3643386ffbc..5ba64ab3eed 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -9,7 +9,7 @@ describe Import::GiteaController do include_context 'a GitHub-ish import controller' def assign_host_url - session[:host_url] = host_url + session[:gitea_host_url] = host_url end describe "GET new" do diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 55820a7cc65..95696e14b6c 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -29,7 +29,7 @@ describe Import::GithubController do get :callback - expect(session[:access_token]).to eq(token) + expect(session[:github_access_token]).to eq(token) expect(controller).to redirect_to(status_import_github_url) end end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index e11ab802095..d0fd2d52004 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -4,6 +4,10 @@ # Note: You have access to `email_value` which is the email address value # being currently tested). +def assign_session_token(provider) + session[:"#{provider}_access_token"] = 'asdasd12345' +end + shared_examples 'a GitHub-ish import controller: POST personal_access_token' do let(:status_import_url) { public_send("status_import_#{provider}_url") } @@ -15,7 +19,7 @@ shared_examples 'a GitHub-ish import controller: POST personal_access_token' do post :personal_access_token, personal_access_token: token - expect(session[:access_token]).to eq(token) + expect(session[:"#{provider}_access_token"]).to eq(token) expect(controller).to redirect_to(status_import_url) end end @@ -24,7 +28,7 @@ shared_examples 'a GitHub-ish import controller: GET new' do let(:status_import_url) { public_send("status_import_#{provider}_url") } it "redirects to status if we already have a token" do - assign_session_token + assign_session_token(provider) allow(controller).to receive(:logged_in_with_provider?).and_return(false) get :new @@ -48,7 +52,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do let(:extra_assign_expectations) { {} } before do - assign_session_token + assign_session_token(provider) end it "assigns variables" do @@ -80,7 +84,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do get :status - expect(session[:access_token]).to eq(nil) + expect(session[:"#{provider}_access_token"]).to be_nil expect(controller).to redirect_to(new_import_url) expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") end @@ -100,11 +104,11 @@ shared_examples 'a GitHub-ish import controller: POST create' do before do stub_client(user: provider_user, repo: provider_repo) - assign_session_token + assign_session_token(provider) end - context "when the repository owner is the Gitea user" do - context "when the Gitea user and GitLab user's usernames match" do + context "when the repository owner is the provider user" do + context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::GithubImport::ProjectCreator). to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). @@ -114,7 +118,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end - context "when the Gitea user and GitLab user's usernames don't match" do + context "when the provider user and GitLab user's usernames don't match" do let(:provider_username) { "someone_else" } it "takes the current user's namespace" do @@ -127,15 +131,15 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end - context "when the repository owner is not the Gitea user" do + context "when the repository owner is not the provider user" do let(:other_username) { "someone_else" } before do provider_repo.owner = OpenStruct.new(login: other_username) - assign_session_token + assign_session_token(provider) end - context "when a namespace with the Gitea user's username already exists" do + context "when a namespace with the provider user's username already exists" do let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } context "when the namespace is owned by the GitLab user" do @@ -164,7 +168,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end - context "when a namespace with the Gitea user's username doesn't exist" do + context "when a namespace with the provider user's username doesn't exist" do context "when current user can create namespaces" do it "creates the namespace" do expect(Gitlab::GithubImport::ProjectCreator). diff --git a/spec/support/import_spec_helper.rb b/spec/support/import_spec_helper.rb index cd25e05ac4b..6710962f082 100644 --- a/spec/support/import_spec_helper.rb +++ b/spec/support/import_spec_helper.rb @@ -30,8 +30,4 @@ module ImportSpecHelper ) allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end - - def assign_session_token - session[:access_token] = 'asdasd12345' - end end -- cgit v1.2.1 From ab06313c36fc5856b2472d3dfcb966a8c6341d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 17:44:22 +0100 Subject: Add Project#gitea_import? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/project.rb | 4 ++++ lib/gitlab/github_import/importer.rb | 6 +++--- lib/gitlab/github_import/milestone_formatter.rb | 2 +- spec/lib/gitlab/github_import/importer_spec.rb | 2 +- spec/models/project_spec.rb | 12 ++++++++++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 5d5d6737dad..0ba8fca24d5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -533,6 +533,10 @@ class Project < ActiveRecord::Base import_type == 'gitlab_project' end + def gitea_import? + import_type == 'gitea' + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' projects_limit = creator.projects_limit diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index c53cd1a928d..ec1318ab33c 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -22,7 +22,7 @@ module Gitlab opts = {} # Gitea plan to be GitHub compliant - if project.import_type == 'gitea' + if project.gitea_import? uri = URI.parse(project.import_url) host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '') opts = { @@ -53,7 +53,7 @@ module Gitlab # Gitea doesn't have a Release API yet # See https://github.com/go-gitea/gitea/issues/330 - unless project.import_type == 'gitea' + unless project.gitea_import? import_releases end @@ -141,7 +141,7 @@ module Gitlab merge_request = gh_pull_request.create! # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage - if project.import_type == 'gitea' + if project.gitea_import? apply_labels(merge_request, raw) end rescue => e diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index fe172b854d5..dd782eff059 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -23,7 +23,7 @@ module Gitlab end def number - if project.import_type == 'gitea' + if project.gitea_import? raw_data.id else raw_data.number diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 0a03b7353f6..72421832ffc 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -192,7 +192,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ] } - unless project.import_type == 'gitea' + unless project.gitea_import? error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" } end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ed6b2c6a22b..8779b399344 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1458,6 +1458,18 @@ describe Project, models: true do end end + describe '#gitlab_project_import?' do + subject(:project) { build(:project, import_type: 'gitlab_project') } + + it { expect(project.gitlab_project_import?).to be true } + end + + describe '#gitea_import?' do + subject(:project) { build(:project, import_type: 'gitea') } + + it { expect(project.gitea_import?).to be true } + end + describe '#lfs_enabled?' do let(:project) { create(:project) } -- cgit v1.2.1 From b67ad2db8717a0f9b673f828f9bea5732e7d258a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 16:54:14 +0000 Subject: 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') } + + 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 - value = find_field('request_url').value - expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + end end end end -- cgit v1.2.1 From 05d04d04980db9d64c7679999200e03a6820bc31 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 19 Dec 2016 17:14:03 +0000 Subject: kamil frontend review changes --- app/helpers/application_helper.rb | 4 ++ app/views/projects/mattermost/_no_teams.html.haml | 12 +++++ .../projects/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 -- cgit v1.2.1 From b1613e54894fdf9176df338f0c1e162075dc80ae Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 19 Dec 2016 17:47:45 +0000 Subject: Makes API call when stage is clicked --- app/assets/javascripts/dispatcher.js.es6 | 15 +++++ .../mini_pipeline_graph_dropdown.js.es6 | 69 ++++++++++++++++++++++ .../projects/ci/pipelines/_pipeline.html.haml | 10 +--- app/views/projects/commit/_pipelines_list.haml | 2 +- app/views/projects/pipelines/_stage.html.haml | 19 ++---- app/views/projects/pipelines/index.html.haml | 2 +- 6 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1e259a16f06..752f35e6356 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -141,6 +141,11 @@ case 'projects:merge_requests:builds': 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(); @@ -158,6 +163,11 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commit:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + break; case 'projects:commit:builds': new gl.Pipelines(); break; @@ -172,6 +182,11 @@ new TreeView(); } break; + case 'projects:pipelines:index': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + break; case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 new file mode 100644 index 00000000000..ce24cbdb705 --- /dev/null +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -0,0 +1,69 @@ +/* global Flash */ + +/** + * In each pipelines table we have a mini pipeline graph for each pipeline. + * + * When we click in a pipeline stage, we need to make an API call to get the + * builds list to render in a dropdown. + * + * The container should be the table element. + * + * The stage icon clicked needs to have the following HTML structure: + *
+ * + *
+ *
+ */ +(() => { + class MiniPipelineGraph { + constructor({ container }) { + this.container = container; + this.getBuildsList = this.getBuildsList.bind(this); + + this.bindEvents(); + } + + /** + * Adds an removes the event listener. + * TODO: Remove jQuery when we have a way to handle events properly. + */ + bindEvents() { + $(this.container).off('click', 'button.js-builds-dropdown-button', this.getBuildsList); + $(this.container).on('click', 'button.js-builds-dropdown-button', this.getBuildsList); + } + + /** + * For the clicked stage, renders the received html in the sibiling + * element with the `js-builds-dropdown-container` clas + * + * @param {Element} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector('.js-builds-dropdown-container'); + + dropdownContainer.innerHTML = data.html; + } + + /** + * For the clicked stage, gets the list of builds. + * + * @param {Object} e + * @return {Promise} + */ + getBuildsList(e) { + const endpoint = e.currentTarget.dataset.stageEndpoint; + + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + success: data => this.renderBuildsList(e.currentTarget, data), + error: () => new Flash('An error occurred while fetching the builds.', 'alert'), + }); + } + } + + window.gl = window.gl || {}; + window.gl.MiniPipelineGraph = MiniPipelineGraph; +})(); diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 74ad9557130..d488eeda2fe 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -53,17 +53,13 @@ .stage-container.mini-pipeline-graph - if hasMultipleBuilds .dropdown.inline.build-content - %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } + %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} %span{ class: klass } %span.mini-pipeline-graph-icon-container %span{ class: icon_status_klass }= custom_icon(icon_status) = icon('caret-down', class: 'dropdown-caret') - .dropdown-menu.grouped-pipeline-dropdown - .arrow-up - %ul - - stage.statuses.each do |status| - %li.dropdown-build - = render 'ci/status/graph_badge', subject: status + + %div.js-builds-dropdown-container - else - if detailed_status.has_details? = link_to detailed_status.details_path, class: klass, title: tooltip do diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 0c2f45c6035..5a9f7295135 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -4,7 +4,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.ci-table + %table.table.ci-table.js-pipeline-table %thead %th.pipeline-status Status %th.pipeline-info Pipeline diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index 44533b77eba..83fd518726d 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,13 +1,6 @@ -- detailed_status = @stage.detailed_status(current_user) -- klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status}" -- hasMultipleBuilds = @stage.statuses.count > 1 -- icon_status = "#{detailed_status.icon}_borderless" -- icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status}" -- tooltip = "#{@stage.name}: #{detailed_status.label || 'not found'}" - -.dropdown.inline.build-content - %button.has-tooltip.builds-dropdown{ type: 'button', data: { toggle: 'dropdown', title: tooltip} } - %span{ class: klass } - %span.mini-pipeline-graph-icon-container - %span{ class: icon_status_klass }= custom_icon(icon_status) - = icon('caret-down', class: 'dropdown-caret') +.dropdown-menu.grouped-pipeline-dropdown + .arrow-up + %ul + - @stage.statuses.each do |status| + %li.dropdown-build + = render 'ci/status/graph_badge', subject: status diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4d009871f0d..28026ccf861 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -42,7 +42,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.ci-table + %table.table.ci-table.js-pipeline-table %thead %th.pipeline-status Status %th.pipeline-info Pipeline -- cgit v1.2.1 From 2e6c1720ead0f2843abb0d03f0c01b92fa063980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 18:21:58 +0100 Subject: Allow Repositories API GET endpoints to be requested anonymously MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../unreleased/4269-public-repositories-api.yml | 4 + doc/api/repositories.md | 18 +- lib/api/repositories.rb | 6 - spec/requests/api/repositories_spec.rb | 276 +++++++++++++++------ 4 files changed, 211 insertions(+), 93 deletions(-) create mode 100644 changelogs/unreleased/4269-public-repositories-api.yml diff --git a/changelogs/unreleased/4269-public-repositories-api.yml b/changelogs/unreleased/4269-public-repositories-api.yml new file mode 100644 index 00000000000..b88ce63845d --- /dev/null +++ b/changelogs/unreleased/4269-public-repositories-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow Repositories API GET endpoints to be requested anonymously +merge_request: +author: diff --git a/doc/api/repositories.md b/doc/api/repositories.md index bcf8b955044..727617f1ecc 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -2,7 +2,8 @@ ## List repository tree -Get a list of repository files and directories in a project. +Get a list of repository files and directories in a project. This endpoint can +be accessed without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/tree @@ -71,7 +72,8 @@ Parameters: ## Raw file content -Get the raw file contents for a file by commit SHA and path. +Get the raw file contents for a file by commit SHA and path. This endpoint can +be accessed without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/blobs/:sha @@ -85,7 +87,8 @@ Parameters: ## Raw blob content -Get the raw file contents for a blob by blob SHA. +Get the raw file contents for a blob by blob SHA. This endpoint can be accessed +without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/raw_blobs/:sha @@ -98,7 +101,8 @@ Parameters: ## Get file archive -Get an archive of the repository +Get an archive of the repository. This endpoint can be accessed without +authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/archive @@ -111,6 +115,9 @@ Parameters: ## Compare branches, tags or commits +This endpoint can be accessed without authentication if the repository is +publicly accessible. + ``` GET /projects/:id/repository/compare ``` @@ -163,7 +170,8 @@ Response: ## Contributors -Get repository contributors list +Get repository contributors list. This endpoint can be accessed without +authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/contributors diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index c287ee34a68..4ca6646a6f1 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -2,7 +2,6 @@ require 'mime/types' module API class Repositories < Grape::API - before { authenticate! } before { authorize! :download_code, user_project } params do @@ -79,8 +78,6 @@ module API optional :format, type: String, desc: 'The archive format' end get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do - authorize! :download_code, user_project - begin send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue @@ -96,7 +93,6 @@ module API requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' end get ':id/repository/compare' do - authorize! :download_code, user_project compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) present compare, with: Entities::Compare end @@ -105,8 +101,6 @@ module API success Entities::Contributor end get ':id/repository/contributors' do - authorize! :download_code, user_project - begin present user_project.repository.contributors, with: Entities::Contributor diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c90b69e8ebb..67f0bc537fe 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -16,15 +16,32 @@ describe API::Repositories, api: true do context "authorized user" do before { project.team << [user2, :reporter] } - it "returns project commits" do - get api("/projects/#{project.id}/repository/tree", user) + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get api("/projects/#{project.id}/repository/tree", current_user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq('bar') - expect(json_response.first['type']).to eq('tree') - expect(json_response.first['mode']).to eq('040000') + first_commit = json_response.first + + expect(json_response).to be_an Array + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end end it 'returns a 404 for unknown ref' do @@ -39,7 +56,8 @@ describe API::Repositories, api: true do context "unauthorized user" do it "does not return project commits" do get api("/projects/#{project.id}/repository/tree") - expect(response).to have_http_status(401) + + expect(response).to have_http_status(404) end end end @@ -72,15 +90,38 @@ describe API::Repositories, api: true do context "unauthorized user" do it "does not return project commits" do get api("/projects/#{project.id}/repository/tree?recursive=1") - expect(response).to have_http_status(401) + + expect(response).to have_http_status(404) end end end - describe "GET /projects/:id/repository/blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) - expect(response).to have_http_status(200) + describe "GET /projects/:id/repository/blobs/:sha & /projects/:id/repository/commits/:sha" do + shared_examples_for 'repository blob' do + it 'returns the repository blob for /repository/blobs/master' do + get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", current_user) + + expect(response).to have_http_status(200) + end + + it 'returns the repository blob for /repository/commits/master' do + get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", current_user) + + expect(response).to have_http_status(200) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository blob' do + let(:current_user) { user } + end end it "returns 404 for invalid branch_name" do @@ -99,17 +140,26 @@ describe API::Repositories, api: true do end end - describe "GET /projects/:id/repository/commits/:sha/blob" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - expect(response).to have_http_status(200) + describe "GET /projects/:id/repository/raw_blobs/:sha" do + shared_examples_for 'repository raw blob' do + it 'returns the repository raw blob' do + get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", current_user) + + expect(response).to have_http_status(200) + end end - end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) - expect(response).to have_http_status(200) + context 'when unauthenticated' do + it_behaves_like 'repository raw blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository raw blob' do + let(:current_user) { user } + end end it 'returns a 404 for unknown blob' do @@ -122,31 +172,55 @@ describe API::Repositories, api: true do end describe "GET /projects/:id/repository/archive(.:format)?:sha" do - it "gets the archive" do - get api("/projects/#{project.id}/repository/archive", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + shared_examples_for 'repository archive' do + it 'returns the repository archive' do + get api("/projects/#{project.id}/repository/archive", current_user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + end + + it 'returns the repository archive archive.zip' do + get api("/projects/#{project.id}/repository/archive.zip", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + end + + it 'returns the repository archive archive.tar.bz2' do + get api("/projects/#{project.id}/repository/archive.tar.bz2", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + end end - it "gets the archive.zip" do - get api("/projects/#{project.id}/repository/archive.zip", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + context 'when unauthenticated' do + it_behaves_like 'repository archive' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "gets the archive.tar.bz2" do - get api("/projects/#{project.id}/repository/archive.tar.bz2", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + context 'when authenticated' do + it_behaves_like 'repository archive' do + let(:current_user) { user } + end end it "returns 404 for invalid sha" do @@ -156,55 +230,93 @@ describe API::Repositories, api: true do end describe 'GET /projects/:id/repository/compare' do - it "compares branches" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end + shared_examples_for 'repository compare' do + it "compares branches" do + get api("/projects/#{project.id}/repository/compare", current_user), from: 'master', to: 'feature' - it "compares tags" do - get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares tags" do + get api("/projects/#{project.id}/repository/compare", current_user), from: 'v1.0.0', to: 'v1.1.0' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares commits" do + get api("/projects/#{project.id}/repository/compare", current_user), from: sample_commit.id, to: sample_commit.parent_id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey + end - it "compares commits" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_falsey + it "compares commits in reverse order" do + get api("/projects/#{project.id}/repository/compare", current_user), from: sample_commit.parent_id, to: sample_commit.id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares same refs" do + get api("/projects/#{project.id}/repository/compare", current_user), from: 'master', to: 'master' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy + end end - it "compares commits in reverse order" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + context 'when unauthenticated' do + it_behaves_like 'repository compare' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "compares same refs" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_truthy + context 'when authenticated' do + it_behaves_like 'repository compare' do + let(:current_user) { user } + end end end describe 'GET /projects/:id/repository/contributors' do - it 'returns valid data' do - get api("/projects/#{project.id}/repository/contributors", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - contributor = json_response.first - expect(contributor['email']).to eq('tiagonbotelho@hotmail.com') - expect(contributor['name']).to eq('tiagonbotelho') - expect(contributor['commits']).to eq(1) - expect(contributor['additions']).to eq(0) - expect(contributor['deletions']).to eq(0) + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get api("/projects/#{project.id}/repository/contributors", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end end end end -- cgit v1.2.1 From e7c56dd1f63a0065b5930accb3ff24314430f82c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 19 Dec 2016 10:25:46 -0500 Subject: Align milestone column header with count number --- app/assets/stylesheets/framework/panels.scss | 14 ++++++++++++++ app/views/shared/milestones/_issuables.html.haml | 8 +++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 5ba0486177f..9d8d08dff88 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -18,6 +18,20 @@ margin-top: -2px; margin-left: 5px; } + + &.split { + display: flex; + align-items: center; + } + + .left { + flex: 1 1 auto; + } + + .right { + flex: 0 0 auto; + text-align: right; + } } .panel-body { diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 8619939dde7..15ff5b8a27e 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -3,10 +3,12 @@ - panel_class = primary ? 'panel-primary' : 'panel-default' .panel{ class: panel_class } - .panel-heading - = title + .panel-heading.split + .left + = title - if show_counter - .pull-right= issuables.size + .right + = issuables.size - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } -- cgit v1.2.1 From 18c9fc42249a08ff28cf9d5b9159b7bada168bcf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 20 Dec 2016 03:24:38 +0800 Subject: Use a block to insert extra check for authenticate_build! Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8091#note_20253762 --- lib/ci/api/helpers.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 62c10c3b753..31fbd1da108 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -14,13 +14,16 @@ module Ci end def authenticate_build!(build) - not_found! unless build - forbidden! unless build_token_valid?(build) - validate_build!(build) + validate_build!(build) do + forbidden! unless build_token_valid?(build) + end end def validate_build!(build) not_found! unless build + + yield if block_given? + forbidden!('Project has been deleted!') unless build.project forbidden!('Build has been erased!') if build.erased? end -- cgit v1.2.1 From c5741e57c2726660732ef2e4114645035025f8c3 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 14 Dec 2016 15:48:46 +0300 Subject: Fix consistent typo in environment.js environmnets => environments --- .../javascripts/environments/components/environment.js.es6 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 88c3d257cea..d04adecd207 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -18,7 +18,7 @@ * The environments array is a recursive tree structure and we need to filter * both root level environments and children environments. * - * In order to acomplish that, both `filterState` and `filterEnvironmnetsByState` + * In order to acomplish that, both `filterState` and `filterEnvironmentsByState` * functions work together. * The first one works as the filter that verifies if the given environment matches * the given state. @@ -34,9 +34,9 @@ * @param {Array} array * @return {Array} */ - const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => { + const filterEnvironmentsByState = (fn, arr) => arr.map((item) => { if (item.children) { - const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean); + const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean); if (filteredChildren.length) { item.children = filteredChildren; return item; @@ -81,7 +81,7 @@ computed: { filteredEnvironments() { - return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments); + return filterEnvironmentsByState(filterState(this.visibility), this.state.environments); }, scope() { @@ -102,7 +102,7 @@ }, /** - * Fetches all the environmnets and stores them. + * Fetches all the environments and stores them. * Toggles loading property. */ created() { -- cgit v1.2.1 From dcddd0f374f9daa321bb8bfa354dc7032d02f5a0 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 14 Dec 2016 15:42:39 +0300 Subject: Remove unnecessary hidden svg elements for icons. --- app/views/projects/environments/index.html.haml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a65a630f2d0..6aae035b3e0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -4,10 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -.commit-icon-svg.hidden - = custom_icon("icon_commit") -.play-icon-svg.hidden - = custom_icon("icon_play") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, -- cgit v1.2.1 From 7c2e16d05319fa79d0b84472130c4a9300e08808 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 24 Nov 2016 13:01:25 +0000 Subject: Add xterm.js 2.1.0 and a wrapper class to the asset pipeline --- app/assets/javascripts/terminal/terminal.js.es6 | 62 + .../javascripts/terminal/terminal_bundle.js.es6 | 5 + config/application.rb | 2 + vendor/assets/javascripts/xterm/fit.js | 86 + vendor/assets/javascripts/xterm/xterm.js | 2235 ++++++++++++++++++++ vendor/assets/stylesheets/xterm/xterm.css | 2206 +++++++++++++++++++ 6 files changed, 4596 insertions(+) create mode 100644 app/assets/javascripts/terminal/terminal.js.es6 create mode 100644 app/assets/javascripts/terminal/terminal_bundle.js.es6 create mode 100644 vendor/assets/javascripts/xterm/fit.js create mode 100644 vendor/assets/javascripts/xterm/xterm.js create mode 100644 vendor/assets/stylesheets/xterm/xterm.css diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6 new file mode 100644 index 00000000000..6b9422b1816 --- /dev/null +++ b/app/assets/javascripts/terminal/terminal.js.es6 @@ -0,0 +1,62 @@ +/* global Terminal */ + +(() => { + class GLTerminal { + + constructor(options) { + this.options = options || {}; + + this.options.cursorBlink = options.cursorBlink || true; + this.options.screenKeys = options.screenKeys || true; + this.container = document.querySelector(options.selector); + + this.setSocketUrl(); + this.createTerminal(); + $(window).off('resize.terminal').on('resize.terminal', () => { + this.terminal.fit(); + }); + } + + setSocketUrl() { + const { protocol, hostname, port } = window.location; + const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://'; + const path = this.container.dataset.projectPath; + + this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`; + } + + createTerminal() { + this.terminal = new Terminal(this.options); + this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); + this.socket.binaryType = 'arraybuffer'; + + this.terminal.open(this.container); + this.socket.onopen = () => { this.runTerminal(); }; + this.socket.onerror = () => { this.handleSocketFailure(); }; + } + + runTerminal() { + const decoder = new TextDecoder('utf-8'); + const encoder = new TextEncoder('utf-8'); + + this.terminal.on('data', (data) => { + this.socket.send(encoder.encode(data)); + }); + + this.socket.addEventListener('message', (ev) => { + this.terminal.write(decoder.decode(ev.data)); + }); + + this.isTerminalInitialized = true; + this.terminal.fit(); + } + + handleSocketFailure() { + this.terminal.write('\r\nConnection failure'); + } + + } + + window.gl = window.gl || {}; + gl.Terminal = GLTerminal; +})(); diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 new file mode 100644 index 00000000000..ded7ee6e9fe --- /dev/null +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -0,0 +1,5 @@ +//= require xterm/xterm.js +//= require xterm/fit.js +//= require ./terminal.js + +$(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/config/application.rb b/config/application.rb index 782a7a36895..057d60ca869 100644 --- a/config/application.rb +++ b/config/application.rb @@ -89,6 +89,7 @@ module Gitlab config.assets.precompile << "mailers/*.css" config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" + config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" @@ -102,6 +103,7 @@ module Gitlab config.assets.precompile << "environments/environments_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" + config.assets.precompile << "terminal/terminal_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js new file mode 100644 index 00000000000..7e24fd9b36e --- /dev/null +++ b/vendor/assets/javascripts/xterm/fit.js @@ -0,0 +1,86 @@ +/* + * Fit terminal columns and rows to the dimensions of its + * DOM element. + * + * Approach: + * - Rows: Truncate the division of the terminal parent element height + * by the terminal row height + * + * - Columns: Truncate the division of the terminal parent element width by + * the terminal character width (apply display: inline at the + * terminal row and truncate its width with the current number + * of columns) + */ +(function (fit) { + if (typeof exports === 'object' && typeof module === 'object') { + /* + * CommonJS environment + */ + module.exports = fit(require('../../xterm')); + } else if (typeof define == 'function') { + /* + * Require.js is available + */ + define(['../../xterm'], fit); + } else { + /* + * Plain browser environment + */ + fit(window.Terminal); + } +})(function (Xterm) { + /** + * This module provides methods for fitting a terminal's size to a parent container. + * + * @module xterm/addons/fit/fit + */ + var exports = {}; + + exports.proposeGeometry = function (term) { + var parentElementStyle = window.getComputedStyle(term.element.parentElement), + parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), + parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')), + elementStyle = window.getComputedStyle(term.element), + elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), + elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), + availableHeight = parentElementHeight - elementPaddingVer, + availableWidth = parentElementWidth - elementPaddingHor, + container = term.rowContainer, + subjectRow = term.rowContainer.firstElementChild, + contentBuffer = subjectRow.innerHTML, + characterHeight, + rows, + characterWidth, + cols, + geometry; + + subjectRow.style.display = 'inline'; + subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace + characterWidth = subjectRow.getBoundingClientRect().width; + subjectRow.style.display = ''; // Revert style before calculating height, since they differ. + characterHeight = parseInt(subjectRow.offsetHeight); + subjectRow.innerHTML = contentBuffer; + + rows = parseInt(availableHeight / characterHeight); + cols = parseInt(availableWidth / characterWidth) - 1; + + geometry = {cols: cols, rows: rows}; + return geometry; + }; + + exports.fit = function (term) { + var geometry = exports.proposeGeometry(term); + + term.resize(geometry.cols, geometry.rows); + }; + + Xterm.prototype.proposeGeometry = function () { + return exports.proposeGeometry(this); + }; + + Xterm.prototype.fit = function () { + return exports.fit(this); + }; + + return exports; +}); diff --git a/vendor/assets/javascripts/xterm/xterm.js b/vendor/assets/javascripts/xterm/xterm.js new file mode 100644 index 00000000000..11ce3c73db9 --- /dev/null +++ b/vendor/assets/javascripts/xterm/xterm.js @@ -0,0 +1,2235 @@ +(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.Terminal = 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 0) { + self.terminal.handler(diff); + } + } + }, 0); +}; + +/** + * Positions the composition view on top of the cursor and the textarea just below it (so the + * IME helper dialog is positioned correctly). + */ +CompositionHelper.prototype.updateCompositionElements = function (dontRecurse) { + if (!this.isComposing) { + return; + } + var cursor = this.terminal.element.querySelector('.terminal-cursor'); + if (cursor) { + // Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within + // the .xterm element. + var xtermRows = this.terminal.element.querySelector('.xterm-rows'); + var cursorTop = xtermRows.offsetTop + cursor.offsetTop; + + this.compositionView.style.left = cursor.offsetLeft + 'px'; + this.compositionView.style.top = cursorTop + 'px'; + this.compositionView.style.height = cursor.offsetHeight + 'px'; + this.compositionView.style.lineHeight = cursor.offsetHeight + 'px'; + // Sync the textarea to the exact position of the composition view so the IME knows where the + // text is. + var compositionViewBounds = this.compositionView.getBoundingClientRect(); + this.textarea.style.left = cursor.offsetLeft + 'px'; + this.textarea.style.top = cursorTop + 'px'; + this.textarea.style.width = compositionViewBounds.width + 'px'; + this.textarea.style.height = compositionViewBounds.height + 'px'; + this.textarea.style.lineHeight = compositionViewBounds.height + 'px'; + } + if (!dontRecurse) { + setTimeout(this.updateCompositionElements.bind(this, true), 0); + } +}; + +/** + * Clears the textarea's position so that the cursor does not blink on IE. + * @private + */ +CompositionHelper.prototype.clearTextareaPosition = function () { + this.textarea.style.left = ''; + this.textarea.style.top = ''; +}; + +exports.CompositionHelper = CompositionHelper; + +},{}],2:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +function EventEmitter() { + this._events = this._events || {}; +} + +EventEmitter.prototype.addListener = function (type, listener) { + this._events[type] = this._events[type] || []; + this._events[type].push(listener); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.removeListener = function (type, listener) { + if (!this._events[type]) return; + + var obj = this._events[type], + i = obj.length; + + while (i--) { + if (obj[i] === listener || obj[i].listener === listener) { + obj.splice(i, 1); + return; + } + } +}; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = function (type) { + if (this._events[type]) delete this._events[type]; +}; + +EventEmitter.prototype.once = function (type, listener) { + var self = this; + function on() { + var args = Array.prototype.slice.call(arguments); + this.removeListener(type, on); + return listener.apply(this, args); + } + on.listener = listener; + return this.on(type, on); +}; + +EventEmitter.prototype.emit = function (type) { + if (!this._events[type]) return; + + var args = Array.prototype.slice.call(arguments, 1), + obj = this._events[type], + l = obj.length, + i = 0; + + for (; i < l; i++) { + obj[i].apply(this, args); + } +}; + +EventEmitter.prototype.listeners = function (type) { + return this._events[type] = this._events[type] || []; +}; + +exports.EventEmitter = EventEmitter; + +},{}],3:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +/** + * Represents the viewport of a terminal, the visible area within the larger buffer of output. + * Logic for the virtual scroll bar is included in this object. + * @param {Terminal} terminal The Terminal object. + * @param {HTMLElement} viewportElement The DOM element acting as the viewport + * @param {HTMLElement} charMeasureElement A DOM element used to measure the character size of + * the terminal. + */ +function Viewport(terminal, viewportElement, scrollArea, charMeasureElement) { + this.terminal = terminal; + this.viewportElement = viewportElement; + this.scrollArea = scrollArea; + this.charMeasureElement = charMeasureElement; + this.currentRowHeight = 0; + this.lastRecordedBufferLength = 0; + this.lastRecordedViewportHeight = 0; + + this.terminal.on('scroll', this.syncScrollArea.bind(this)); + this.terminal.on('resize', this.syncScrollArea.bind(this)); + this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); + + this.syncScrollArea(); +} + +/** + * Refreshes row height, setting line-height, viewport height and scroll area height if + * necessary. + * @param {number|undefined} charSize A character size measurement bounding rect object, if it + * doesn't exist it will be created. + */ +Viewport.prototype.refresh = function (charSize) { + var size = charSize || this.charMeasureElement.getBoundingClientRect(); + if (size.height > 0) { + var rowHeightChanged = size.height !== this.currentRowHeight; + if (rowHeightChanged) { + this.currentRowHeight = size.height; + this.viewportElement.style.lineHeight = size.height + 'px'; + this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + } + var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + if (rowHeightChanged || viewportHeightChanged) { + this.lastRecordedViewportHeight = this.terminal.rows; + this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + } + this.scrollArea.style.height = size.height * this.lastRecordedBufferLength + 'px'; + } +}; + +/** + * Updates dimensions and synchronizes the scroll area if necessary. + */ +Viewport.prototype.syncScrollArea = function () { + if (this.lastRecordedBufferLength !== this.terminal.lines.length) { + // If buffer height changed + this.lastRecordedBufferLength = this.terminal.lines.length; + this.refresh(); + } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { + // If viewport height changed + this.refresh(); + } else { + // If size has changed, refresh viewport + var size = this.charMeasureElement.getBoundingClientRect(); + if (size.height !== this.currentRowHeight) { + this.refresh(size); + } + } + + // Sync scrollTop + var scrollTop = this.terminal.ydisp * this.currentRowHeight; + if (this.viewportElement.scrollTop !== scrollTop) { + this.viewportElement.scrollTop = scrollTop; + } +}; + +/** + * Handles scroll events on the viewport, calculating the new viewport and requesting the + * terminal to scroll to it. + * @param {Event} ev The scroll event. + */ +Viewport.prototype.onScroll = function (ev) { + var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); + var diff = newRow - this.terminal.ydisp; + this.terminal.scrollDisp(diff, true); +}; + +/** + * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual + * scrolling to `onScroll`, this event needs to be attached manually by the consumer of + * `Viewport`. + * @param {WheelEvent} ev The mouse wheel event. + */ +Viewport.prototype.onWheel = function (ev) { + if (ev.deltaY === 0) { + // Do nothing if it's not a vertical scroll event + return; + } + // Fallback to WheelEvent.DOM_DELTA_PIXEL + var multiplier = 1; + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + multiplier = this.currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + multiplier = this.currentRowHeight * this.terminal.rows; + } + this.viewportElement.scrollTop += ev.deltaY * multiplier; + // Prevent the page from scrolling when the terminal scrolls + ev.preventDefault(); +}; + +exports.Viewport = Viewport; + +},{}],4:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company (MIT License) + */ + +/** + * Clipboard handler module. This module contains methods for handling all + * clipboard-related events appropriately in the terminal. + * @module xterm/handlers/Clipboard + */ + +/** + * Prepares text copied from terminal selection, to be saved in the clipboard by: + * 1. stripping all trailing white spaces + * 2. converting all non-breaking spaces to regular spaces + * @param {string} text The copied text that needs processing for storing in clipboard + * @returns {string} + */ +function prepareTextForClipboard(text) { + var space = String.fromCharCode(32), + nonBreakingSpace = String.fromCharCode(160), + allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), + processedText = text.split('\n').map(function (line) { + // Strip all trailing white spaces and convert all non-breaking spaces + // to regular spaces. + var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space); + + return processedLine; + }).join('\n'); + + return processedText; +} + +/** + * Binds copy functionality to the given terminal. + * @param {ClipboardEvent} ev The original copy event to be handled + */ +function copyHandler(ev, term) { + var copiedText = window.getSelection().toString(), + text = prepareTextForClipboard(copiedText); + + if (term.browser.isMSIE) { + window.clipboardData.setData('Text', text); + } else { + ev.clipboardData.setData('text/plain', text); + } + + ev.preventDefault(); // Prevent or the original text will be copied. +} + +/** + * Redirect the clipboard's data to the terminal's input handler. + * @param {ClipboardEvent} ev The original paste event to be handled + * @param {Terminal} term The terminal on which to apply the handled paste event + */ +function pasteHandler(ev, term) { + ev.stopPropagation(); + + var dispatchPaste = function dispatchPaste(text) { + term.handler(text); + term.textarea.value = ''; + return term.cancel(ev); + }; + + if (term.browser.isMSIE) { + if (window.clipboardData) { + var text = window.clipboardData.getData('Text'); + dispatchPaste(text); + } + } else { + if (ev.clipboardData) { + var text = ev.clipboardData.getData('text/plain'); + dispatchPaste(text); + } + } +} + +/** + * Bind to right-click event and allow right-click copy and paste. + * + * **Logic** + * If text is selected and right-click happens on selected text, then + * do nothing to allow seamless copying. + * If no text is selected or right-click is outside of the selection + * area, then bring the terminal's input below the cursor, in order to + * trigger the event on the textarea and allow-right click paste, without + * caring about disappearing selection. + * @param {ClipboardEvent} ev The original paste event to be handled + * @param {Terminal} term The terminal on which to apply the handled paste event + */ +function rightClickHandler(ev, term) { + var s = document.getSelection(), + selectedText = prepareTextForClipboard(s.toString()), + clickIsOnSelection = false; + + if (s.rangeCount) { + var r = s.getRangeAt(0), + cr = r.getClientRects(), + x = ev.clientX, + y = ev.clientY, + i, + rect; + + for (i = 0; i < cr.length; i++) { + rect = cr[i]; + clickIsOnSelection = x > rect.left && x < rect.right && y > rect.top && y < rect.bottom; + + if (clickIsOnSelection) { + break; + } + } + // If we clicked on selection and selection is not a single space, + // then mark the right click as copy-only. We check for the single + // space selection, as this can happen when clicking on an   + // and there is not much pointing in copying a single space. + if (selectedText.match(/^\s$/) || !selectedText.length) { + clickIsOnSelection = false; + } + } + + // Bring textarea at the cursor position + if (!clickIsOnSelection) { + term.textarea.style.position = 'fixed'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = x - 10 + 'px'; + term.textarea.style.top = y - 10 + 'px'; + term.textarea.style.zIndex = 1000; + term.textarea.focus(); + + // Reset the terminal textarea's styling + setTimeout(function () { + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; + }, 4); + } +} + +exports.prepareTextForClipboard = prepareTextForClipboard; +exports.copyHandler = copyHandler; +exports.pasteHandler = pasteHandler; +exports.rightClickHandler = rightClickHandler; + +},{}],5:[function(_dereq_,module,exports){ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isMSWindows = exports.isIphone = exports.isIpad = exports.isMac = exports.isMSIE = exports.isFirefox = undefined; + +var _Generic = _dereq_('./Generic.js'); + +var isNode = typeof navigator == 'undefined' ? true : false; /** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company (MIT License) + */ + +/** + * Browser utilities module. This module contains attributes and methods to help with + * identifying the current browser and platform. + * @module xterm/utils/Browser + */ + +var userAgent = isNode ? 'node' : navigator.userAgent; +var platform = isNode ? 'node' : navigator.platform; + +var isFirefox = exports.isFirefox = !!~userAgent.indexOf('Firefox'); +var isMSIE = exports.isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +var isMac = exports.isMac = (0, _Generic.contains)(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); +var isIpad = exports.isIpad = platform === 'iPad'; +var isIphone = exports.isIphone = platform === 'iPhone'; +var isMSWindows = exports.isMSWindows = (0, _Generic.contains)(['Windows', 'Win16', 'Win32', 'WinCE'], platform); + +},{"./Generic.js":6}],6:[function(_dereq_,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company (MIT License) + */ + +/** + * Generic utilities module. This module contains generic methods that can be helpful at + * different parts of the code base. + * @module xterm/utils/Generic + */ + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +var contains = exports.contains = function contains(arr, el) { + return arr.indexOf(el) >= 0; +}; + +},{}],7:[function(_dereq_,module,exports){ +'use strict';var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj;}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj;};/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2014, SourceLair Private Company (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */var _CompositionHelper=_dereq_('./CompositionHelper.js');var _EventEmitter=_dereq_('./EventEmitter.js');var _Viewport=_dereq_('./Viewport.js');var _Clipboard=_dereq_('./handlers/Clipboard.js');var _Browser=_dereq_('./utils/Browser');var Browser=_interopRequireWildcard(_Browser);function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj;}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key];}}newObj.default=obj;return newObj;}}/** + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */// Let it work inside Node.js for automated testing purposes. +var document=typeof window!='undefined'?window.document:null;/** + * States + */var normal=0,escaped=1,csi=2,osc=3,charset=4,dcs=5,ignore=6;/** + * Terminal + *//** + * Creates a new `Terminal` object. + * + * @param {object} options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */function Terminal(options){var self=this;if(!(this instanceof Terminal)){return new Terminal(arguments[0],arguments[1],arguments[2]);}self.browser=Browser;self.cancel=Terminal.cancel;_EventEmitter.EventEmitter.call(this);if(typeof options==='number'){options={cols:arguments[0],rows:arguments[1],handler:arguments[2]};}options=options||{};Object.keys(Terminal.defaults).forEach(function(key){if(options[key]==null){options[key]=Terminal.options[key];if(Terminal[key]!==Terminal.defaults[key]){options[key]=Terminal[key];}}self[key]=options[key];});if(options.colors.length===8){options.colors=options.colors.concat(Terminal._colors.slice(8));}else if(options.colors.length===16){options.colors=options.colors.concat(Terminal._colors.slice(16));}else if(options.colors.length===10){options.colors=options.colors.slice(0,-2).concat(Terminal._colors.slice(8,-2),options.colors.slice(-2));}else if(options.colors.length===18){options.colors=options.colors.concat(Terminal._colors.slice(16,-2),options.colors.slice(-2));}this.colors=options.colors;this.options=options;// this.context = options.context || window; +// this.document = options.document || document; +this.parent=options.body||options.parent||(document?document.getElementsByTagName('body')[0]:null);this.cols=options.cols||options.geometry[0];this.rows=options.rows||options.geometry[1];this.geometry=[this.cols,this.rows];if(options.handler){this.on('data',options.handler);}/** + * The scroll position of the y cursor, ie. ybase + y = the y position within the entire + * buffer + */this.ybase=0;/** + * The scroll position of the viewport + */this.ydisp=0;/** + * The cursor's x position after ybase + */this.x=0;/** + * The cursor's y position after ybase + */this.y=0;/** + * Used to debounce the refresh function + */this.isRefreshing=false;/** + * Whether there is a full terminal refresh queued + */this.cursorState=0;this.cursorHidden=false;this.convertEol;this.state=0;this.queue='';this.scrollTop=0;this.scrollBottom=this.rows-1;this.customKeydownHandler=null;// modes +this.applicationKeypad=false;this.applicationCursor=false;this.originMode=false;this.insertMode=false;this.wraparoundMode=true;// defaults: xterm - true, vt100 - false +this.normal=null;// charset +this.charset=null;this.gcharset=null;this.glevel=0;this.charsets=[null];// mouse properties +this.decLocator;this.x10Mouse;this.vt200Mouse;this.vt300Mouse;this.normalMouse;this.mouseEvents;this.sendFocus;this.utfMouse;this.sgrMouse;this.urxvtMouse;// misc +this.element;this.children;this.refreshStart;this.refreshEnd;this.savedX;this.savedY;this.savedCols;// stream +this.readable=true;this.writable=true;this.defAttr=0<<18|257<<9|256<<0;this.curAttr=this.defAttr;this.params=[];this.currentParam=0;this.prefix='';this.postfix='';// leftover surrogate high from previous write invocation +this.surrogate_high='';/** + * An array of all lines in the entire buffer, including the prompt. The lines are array of + * characters which are 2-length arrays where [0] is an attribute and [1] is the character. + */this.lines=[];var i=this.rows;while(i--){this.lines.push(this.blankLine());}this.tabs;this.setupStops();// Store if user went browsing history in scrollback +this.userScrolling=false;}inherits(Terminal,_EventEmitter.EventEmitter);/** + * back_color_erase feature for xterm. + */Terminal.prototype.eraseAttr=function(){// if (this.is('screen')) return this.defAttr; +return this.defAttr&~0x1ff|this.curAttr&0x1ff;};/** + * Colors + */// Colors 0-15 +Terminal.tangoColors=[// dark: +'#2e3436','#cc0000','#4e9a06','#c4a000','#3465a4','#75507b','#06989a','#d3d7cf',// bright: +'#555753','#ef2929','#8ae234','#fce94f','#729fcf','#ad7fa8','#34e2e2','#eeeeec'];// Colors 0-15 + 16-255 +// Much thanks to TooTallNate for writing this. +Terminal.colors=function(){var colors=Terminal.tangoColors.slice(),r=[0x00,0x5f,0x87,0xaf,0xd7,0xff],i;// 16-231 +i=0;for(;i<216;i++){out(r[i/36%6|0],r[i/6%6|0],r[i%6]);}// 232-255 (grey) +i=0;for(;i<24;i++){r=8+i*10;out(r,r,r);}function out(r,g,b){colors.push('#'+hex(r)+hex(g)+hex(b));}function hex(c){c=c.toString(16);return c.length<2?'0'+c:c;}return colors;}();Terminal._colors=Terminal.colors.slice();Terminal.vcolors=function(){var out=[],colors=Terminal.colors,i=0,color;for(;i<256;i++){color=parseInt(colors[i].substring(1),16);out.push([color>>16&0xff,color>>8&0xff,color&0xff]);}return out;}();/** + * Options + */Terminal.defaults={colors:Terminal.colors,theme:'default',convertEol:false,termName:'xterm',geometry:[80,24],cursorBlink:false,visualBell:false,popOnBell:false,scrollback:1000,screenKeys:false,debug:false,cancelEvents:false// programFeatures: false, +// focusKeys: false, +};Terminal.options={};Terminal.focus=null;each(keys(Terminal.defaults),function(key){Terminal[key]=Terminal.defaults[key];Terminal.options[key]=Terminal.defaults[key];});/** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */Terminal.prototype.focus=function(){return this.textarea.focus();};/** + * Retrieves an option's value from the terminal. + * @param {string} key The option key. + */Terminal.prototype.getOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}if(typeof this.options[key]!=='undefined'){return this.options[key];}return this[key];};/** + * Sets an option on the terminal. + * @param {string} key The option key. + * @param {string} value The option value. + */Terminal.prototype.setOption=function(key,value){if(!(key in Terminal.defaults)){throw new Error('No option with key "'+key+'"');}this[key]=value;this.options[key]=value;};/** + * Binds the desired focus behavior on a given terminal object. + * + * @static + */Terminal.bindFocus=function(term){on(term.textarea,'focus',function(ev){if(term.sendFocus){term.send('\x1b[I');}term.element.classList.add('focus');term.showCursor();Terminal.focus=term;term.emit('focus',{terminal:term});});};/** + * Blur the terminal. Delegates blur handling to the terminal's DOM element. + */Terminal.prototype.blur=function(){return this.textarea.blur();};/** + * Binds the desired blur behavior on a given terminal object. + * + * @static + */Terminal.bindBlur=function(term){on(term.textarea,'blur',function(ev){term.refresh(term.y,term.y);if(term.sendFocus){term.send('\x1b[O');}term.element.classList.remove('focus');Terminal.focus=null;term.emit('blur',{terminal:term});});};/** + * Initialize default behavior + */Terminal.prototype.initGlobal=function(){var term=this;Terminal.bindKeys(this);Terminal.bindFocus(this);Terminal.bindBlur(this);// Bind clipboard functionality +on(this.element,'copy',function(ev){_Clipboard.copyHandler.call(this,ev,term);});on(this.textarea,'paste',function(ev){_Clipboard.pasteHandler.call(this,ev,term);});function rightClickHandlerWrapper(ev){_Clipboard.rightClickHandler.call(this,ev,term);}if(term.browser.isFirefox){on(this.element,'mousedown',function(ev){if(ev.button==2){rightClickHandlerWrapper(ev);}});}else{on(this.element,'contextmenu',rightClickHandlerWrapper);}};/** + * Apply key handling to the terminal + */Terminal.bindKeys=function(term){on(term.element,'keydown',function(ev){if(document.activeElement!=this){return;}term.keyDown(ev);},true);on(term.element,'keypress',function(ev){if(document.activeElement!=this){return;}term.keyPress(ev);},true);on(term.element,'keyup',term.focus.bind(term));on(term.textarea,'keydown',function(ev){term.keyDown(ev);},true);on(term.textarea,'keypress',function(ev){term.keyPress(ev);// Truncate the textarea's value, since it is not needed +this.value='';},true);on(term.textarea,'compositionstart',term.compositionHelper.compositionstart.bind(term.compositionHelper));on(term.textarea,'compositionupdate',term.compositionHelper.compositionupdate.bind(term.compositionHelper));on(term.textarea,'compositionend',term.compositionHelper.compositionend.bind(term.compositionHelper));term.on('refresh',term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));};/** + * Insert the given row to the terminal or produce a new one + * if no row argument is passed. Return the inserted row. + * @param {HTMLElement} row (optional) The row to append to the terminal. + */Terminal.prototype.insertRow=function(row){if((typeof row==='undefined'?'undefined':_typeof(row))!='object'){row=document.createElement('div');}this.rowContainer.appendChild(row);this.children.push(row);return row;};/** + * Opens the terminal within an element. + * + * @param {HTMLElement} parent The element to create the terminal within. + */Terminal.prototype.open=function(parent){var self=this,i=0,div;this.parent=parent||this.parent;if(!this.parent){throw new Error('Terminal requires a parent element.');}// Grab global elements +this.context=this.parent.ownerDocument.defaultView;this.document=this.parent.ownerDocument;this.body=this.document.getElementsByTagName('body')[0];//Create main element container +this.element=this.document.createElement('div');this.element.classList.add('terminal');this.element.classList.add('xterm');this.element.classList.add('xterm-theme-'+this.theme);this.element.style.height;this.element.setAttribute('tabindex',0);this.viewportElement=document.createElement('div');this.viewportElement.classList.add('xterm-viewport');this.element.appendChild(this.viewportElement);this.viewportScrollArea=document.createElement('div');this.viewportScrollArea.classList.add('xterm-scroll-area');this.viewportElement.appendChild(this.viewportScrollArea);// Create the container that will hold the lines of the terminal and then +// produce the lines the lines. +this.rowContainer=document.createElement('div');this.rowContainer.classList.add('xterm-rows');this.element.appendChild(this.rowContainer);this.children=[];// Create the container that will hold helpers like the textarea for +// capturing DOM Events. Then produce the helpers. +this.helperContainer=document.createElement('div');this.helperContainer.classList.add('xterm-helpers');// TODO: This should probably be inserted once it's filled to prevent an additional layout +this.element.appendChild(this.helperContainer);this.textarea=document.createElement('textarea');this.textarea.classList.add('xterm-helper-textarea');this.textarea.setAttribute('autocorrect','off');this.textarea.setAttribute('autocapitalize','off');this.textarea.setAttribute('spellcheck','false');this.textarea.tabIndex=0;this.textarea.addEventListener('focus',function(){self.emit('focus',{terminal:self});});this.textarea.addEventListener('blur',function(){self.emit('blur',{terminal:self});});this.helperContainer.appendChild(this.textarea);this.compositionView=document.createElement('div');this.compositionView.classList.add('composition-view');this.compositionHelper=new _CompositionHelper.CompositionHelper(this.textarea,this.compositionView,this);this.helperContainer.appendChild(this.compositionView);this.charMeasureElement=document.createElement('div');this.charMeasureElement.classList.add('xterm-char-measure-element');this.charMeasureElement.innerHTML='W';this.helperContainer.appendChild(this.charMeasureElement);for(;i +function sendButton(ev){var button,pos;// get the xterm-style button +button=getButton(ev);// get mouse coordinates +pos=getCoords(ev);if(!pos)return;sendEvent(button,pos);switch(ev.overrideType||ev.type){case'mousedown':pressed=button;break;case'mouseup':// keep it at the left +// button, just in case. +pressed=32;break;case'wheel':// nothing. don't +// interfere with +// `pressed`. +break;}}// motion example of a left click: +// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< +function sendMove(ev){var button=pressed,pos;pos=getCoords(ev);if(!pos)return;// buttons marked as motions +// are incremented by 32 +button+=32;sendEvent(button,pos);}// encode button and +// position to characters +function encode(data,ch){if(!self.utfMouse){if(ch===255)return data.push(0);if(ch>127)ch=127;data.push(ch);}else{if(ch===2047)return data.push(0);if(ch<127){data.push(ch);}else{if(ch>2047)ch=2047;data.push(0xC0|ch>>6);data.push(0x80|ch&0x3F);}}}// send a mouse event: +// regular/utf8: ^[[M Cb Cx Cy +// urxvt: ^[[ Cb ; Cx ; Cy M +// sgr: ^[[ Cb ; Cx ; Cy M/m +// vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r +// locator: CSI P e ; P b ; P r ; P c ; P p & w +function sendEvent(button,pos){// self.emit('mouse', { +// x: pos.x - 32, +// y: pos.x - 32, +// button: button +// }); +if(self.vt300Mouse){// NOTE: Unstable. +// http://www.vt100.net/docs/vt3xx-gp/chapter15.html +button&=3;pos.x-=32;pos.y-=32;var data='\x1b[24';if(button===0)data+='1';else if(button===1)data+='3';else if(button===2)data+='5';else if(button===3)return;else data+='0';data+='~['+pos.x+','+pos.y+']\r';self.send(data);return;}if(self.decLocator){// NOTE: Unstable. +button&=3;pos.x-=32;pos.y-=32;if(button===0)button=2;else if(button===1)button=4;else if(button===2)button=6;else if(button===3)button=3;self.send('\x1b['+button+';'+(button===3?4:0)+';'+pos.y+';'+pos.x+';'+(pos.page||0)+'&w');return;}if(self.urxvtMouse){pos.x-=32;pos.y-=32;pos.x++;pos.y++;self.send('\x1b['+button+';'+pos.x+';'+pos.y+'M');return;}if(self.sgrMouse){pos.x-=32;pos.y-=32;self.send('\x1b[<'+((button&3)===3?button&~3:button)+';'+pos.x+';'+pos.y+((button&3)===3?'m':'M'));return;}var data=[];encode(data,button);encode(data,pos.x);encode(data,pos.y);self.send('\x1b[M'+String.fromCharCode.apply(String,data));}function getButton(ev){var button,shift,meta,ctrl,mod;// two low bits: +// 0 = left +// 1 = middle +// 2 = right +// 3 = release +// wheel up/down: +// 1, and 2 - with 64 added +switch(ev.overrideType||ev.type){case'mousedown':button=ev.button!=null?+ev.button:ev.which!=null?ev.which-1:null;if(self.browser.isMSIE){button=button===1?0:button===4?1:button;}break;case'mouseup':button=3;break;case'DOMMouseScroll':button=ev.detail<0?64:65;break;case'wheel':button=ev.wheelDeltaY>0?64:65;break;}// next three bits are the modifiers: +// 4 = shift, 8 = meta, 16 = control +shift=ev.shiftKey?4:0;meta=ev.metaKey?8:0;ctrl=ev.ctrlKey?16:0;mod=shift|meta|ctrl;// no mods +if(self.vt200Mouse){// ctrl only +mod&=ctrl;}else if(!self.normalMouse){mod=0;}// increment to SP +button=32+(mod<<2)+button;return button;}// mouse coordinates measured in cols/rows +function getCoords(ev){var x,y,w,h,el;// ignore browsers without pageX for now +if(ev.pageX==null)return;x=ev.pageX;y=ev.pageY;el=self.element;// should probably check offsetParent +// but this is more portable +while(el&&el!==self.document.documentElement){x-=el.offsetLeft;y-=el.offsetTop;el='offsetParent'in el?el.offsetParent:el.parentNode;}// convert to cols/rows +w=self.element.clientWidth;h=self.element.clientHeight;x=Math.ceil(x/w*self.cols);y=Math.ceil(y/h*self.rows);// be sure to avoid sending +// bad positions to the program +if(x<0)x=0;if(x>self.cols)x=self.cols;if(y<0)y=0;if(y>self.rows)y=self.rows;// xterm sends raw bytes and +// starts at 32 (SP) for each. +x+=32;y+=32;return{x:x,y:y,type:'wheel'};}on(el,'mousedown',function(ev){if(!self.mouseEvents)return;// send the button +sendButton(ev);// ensure focus +self.focus();// fix for odd bug +//if (self.vt200Mouse && !self.normalMouse) { +if(self.vt200Mouse){ev.overrideType='mouseup';sendButton(ev);return self.cancel(ev);}// bind events +if(self.normalMouse)on(self.document,'mousemove',sendMove);// x10 compatibility mode can't send button releases +if(!self.x10Mouse){on(self.document,'mouseup',function up(ev){sendButton(ev);if(self.normalMouse)off(self.document,'mousemove',sendMove);off(self.document,'mouseup',up);return self.cancel(ev);});}return self.cancel(ev);});//if (self.normalMouse) { +// on(self.document, 'mousemove', sendMove); +//} +on(el,'wheel',function(ev){if(!self.mouseEvents)return;if(self.x10Mouse||self.vt300Mouse||self.decLocator)return;sendButton(ev);return self.cancel(ev);});// allow wheel scrolling in +// the shell for example +on(el,'wheel',function(ev){if(self.mouseEvents)return;self.viewport.onWheel(ev);return self.cancel(ev);});};/** + * Destroys the terminal. + */Terminal.prototype.destroy=function(){this.readable=false;this.writable=false;this._events={};this.handler=function(){};this.write=function(){};if(this.element.parentNode){this.element.parentNode.removeChild(this.element);}//this.emit('close'); +};/** + * Flags used to render terminal text properly + */Terminal.flags={BOLD:1,UNDERLINE:2,BLINK:4,INVERSE:8,INVISIBLE:16};/** + * Refreshes (re-renders) terminal content within two rows (inclusive) + * + * Rendering Engine: + * + * In the screen buffer, each character is stored as a an array with a character + * and a 32-bit integer: + * - First value: a utf-16 character. + * - Second value: + * - Next 9 bits: background color (0-511). + * - Next 9 bits: foreground color (0-511). + * - Next 14 bits: a mask for misc. flags: + * - 1=bold + * - 2=underline + * - 4=blink + * - 8=inverse + * - 16=invisible + * + * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) + * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) + * @param {boolean} queue Whether the refresh should ran right now or be queued + */Terminal.prototype.refresh=function(start,end,queue){var self=this;// queue defaults to true +queue=typeof queue=='undefined'?true:queue;/** + * The refresh queue allows refresh to execute only approximately 30 times a second. For + * commands that pass a significant amount of output to the write function, this prevents the + * terminal from maxing out the CPU and making the UI unresponsive. While commands can still + * run beyond what they do on the terminal, it is far better with a debounce in place as + * every single terminal manipulation does not need to be constructed in the DOM. + * + * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. + */if(queue){// If refresh should be queued, order the refresh and return. +if(this._refreshIsQueued){// If a refresh has already been queued, just order a full refresh next +this._fullRefreshNext=true;}else{setTimeout(function(){self.refresh(start,end,false);},34);this._refreshIsQueued=true;}return;}// If refresh should be run right now (not be queued), release the lock +this._refreshIsQueued=false;// If multiple refreshes were requested, make a full refresh. +if(this._fullRefreshNext){start=0;end=this.rows-1;this._fullRefreshNext=false;// reset lock +}var x,y,i,line,out,ch,ch_width,width,data,attr,bg,fg,flags,row,parent,focused=document.activeElement;// If this is a big refresh, remove the terminal rows from the DOM for faster calculations +if(end-start>=this.rows/2){parent=this.element.parentNode;if(parent){this.element.removeChild(this.rowContainer);}}width=this.cols;y=start;if(end>=this.rows.length){this.log('`end` is too large. Most likely a bad CSR.');end=this.rows.length-1;}for(;y<=end;y++){row=y+this.ydisp;line=this.lines[row];out='';if(this.y===y-(this.ybase-this.ydisp)&&this.cursorState&&!this.cursorHidden){x=this.x;}else{x=-1;}attr=this.defAttr;i=0;for(;i';}if(data!==this.defAttr){if(data===-1){out+='';}else{var classNames=[];bg=data&0x1ff;fg=data>>9&0x1ff;flags=data>>18;if(flags&Terminal.flags.BOLD){if(!Terminal.brokenBold){classNames.push('xterm-bold');}// See: XTerm*boldColors +if(fg<8)fg+=8;}if(flags&Terminal.flags.UNDERLINE){classNames.push('xterm-underline');}if(flags&Terminal.flags.BLINK){classNames.push('xterm-blink');}// If inverse flag is on, then swap the foreground and background variables. +if(flags&Terminal.flags.INVERSE){/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */bg=[fg,fg=bg][0];// Should inverse just be before the +// above boldColors effect instead? +if(flags&1&&fg<8)fg+=8;}if(flags&Terminal.flags.INVISIBLE){classNames.push('xterm-hidden');}/** + * Weird situation: Invert flag used black foreground and white background results + * in invalid background color, positioned at the 256 index of the 256 terminal + * color map. Pin the colors manually in such a case. + * + * Source: https://github.com/sourcelair/xterm.js/issues/57 + */if(flags&Terminal.flags.INVERSE){if(bg==257){bg=15;}if(fg==256){fg=0;}}if(bg<256){classNames.push('xterm-bg-color-'+bg);}if(fg<256){classNames.push('xterm-color-'+fg);}out+='':out+='>';break;default:if(ch<=' '){out+=' ';}else{out+=ch;}break;}attr=data;}if(attr!==this.defAttr){out+='';}this.children[y].innerHTML=out;}if(parent){this.element.appendChild(this.rowContainer);}this.emit('refresh',{element:this.element,start:start,end:end});};/** + * Display the cursor element + */Terminal.prototype.showCursor=function(){if(!this.cursorState){this.cursorState=1;this.refresh(this.y,this.y);}};/** + * Scroll the terminal + */Terminal.prototype.scroll=function(){var row;if(++this.ybase===this.scrollback){this.ybase=this.ybase/2|0;this.lines=this.lines.slice(-(this.ybase+this.rows)+1);}if(!this.userScrolling){this.ydisp=this.ybase;}// last line +row=this.ybase+this.rows-1;// subtract the bottom scroll region +row-=this.rows-1-this.scrollBottom;if(row===this.lines.length){// potential optimization: +// pushing is faster than splicing +// when they amount to the same +// behavior. +this.lines.push(this.blankLine());}else{// add our new line +this.lines.splice(row,0,this.blankLine());}if(this.scrollTop!==0){if(this.ybase!==0){this.ybase--;if(!this.userScrolling){this.ydisp=this.ybase;}}this.lines.splice(this.ybase+this.scrollTop,1);}// this.maxRange(); +this.updateRange(this.scrollTop);this.updateRange(this.scrollBottom);this.emit('scroll',this.ydisp);};/** + * Scroll the display of the terminal + * @param {number} disp The number of lines to scroll down (negatives scroll up). + * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used + * to avoid unwanted events being handled by the veiwport when the event was triggered from the + * viewport originally. + */Terminal.prototype.scrollDisp=function(disp,suppressScrollEvent){if(disp<0){this.userScrolling=true;}else if(disp+this.ydisp>=this.ybase){this.userScrolling=false;}this.ydisp+=disp;if(this.ydisp>this.ybase){this.ydisp=this.ybase;}else if(this.ydisp<0){this.ydisp=0;}if(!suppressScrollEvent){this.emit('scroll',this.ydisp);}this.refresh(0,this.rows-1);};/** + * Scroll the display of the terminal by a number of pages. + * @param {number} pageCount The number of pages to scroll (negative scrolls up). + */Terminal.prototype.scrollPages=function(pageCount){this.scrollDisp(pageCount*(this.rows-1));};/** + * Scrolls the display of the terminal to the top. + */Terminal.prototype.scrollToTop=function(){this.scrollDisp(-this.ydisp);};/** + * Scrolls the display of the terminal to the bottom. + */Terminal.prototype.scrollToBottom=function(){this.scrollDisp(this.ybase-this.ydisp);};/** + * Writes text to the terminal. + * @param {string} text The text to write to the terminal. + */Terminal.prototype.write=function(data){var l=data.length,i=0,j,cs,ch,code,low,ch_width,row;this.refreshStart=this.y;this.refreshEnd=this.y;// apply leftover surrogate high from last write +if(this.surrogate_high){data=this.surrogate_high+data;this.surrogate_high='';}for(;i maybe move to default +code=data.charCodeAt(i);if(0xD800<=code&&code<=0xDBFF){// we got a surrogate high +// get surrogate low (next 2 bytes) +low=data.charCodeAt(i+1);if(isNaN(low)){// end of data stream, save surrogate high +this.surrogate_high=ch;continue;}code=(code-0xD800)*0x400+(low-0xDC00)+0x10000;ch+=data.charAt(i+1);}// surrogate low - already handled above +if(0xDC00<=code&&code<=0xDFFF)continue;switch(this.state){case normal:switch(ch){case'\x07':this.bell();break;// '\n', '\v', '\f' +case'\n':case'\x0b':case'\x0c':if(this.convertEol){this.x=0;}this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}break;// '\r' +case'\r':this.x=0;break;// '\b' +case'\x08':if(this.x>0){this.x--;}break;// '\t' +case'\t':this.x=this.nextStop();break;// shift out +case'\x0e':this.setgLevel(1);break;// shift in +case'\x0f':this.setgLevel(0);break;// '\e' +case'\x1b':this.state=escaped;break;default:// ' ' +// calculate print space +// expensive call, therefore we save width in line buffer +ch_width=wcwidth(code);if(ch>=' '){if(this.charset&&this.charset[ch]){ch=this.charset[ch];}row=this.y+this.ybase;// insert combining char in last cell +// FIXME: needs handling after cursor jumps +if(!ch_width&&this.x){// dont overflow left +if(this.lines[row][this.x-1]){if(!this.lines[row][this.x-1][2]){// found empty cell after fullwidth, need to go 2 cells back +if(this.lines[row][this.x-2])this.lines[row][this.x-2][1]+=ch;}else{this.lines[row][this.x-1][1]+=ch;}this.updateRange(this.y);}break;}// goto next line if ch would overflow +// TODO: needs a global min terminal width of 2 +if(this.x+ch_width-1>=this.cols){// autowrap - DECAWM +if(this.wraparoundMode){this.x=0;this.y++;if(this.y>this.scrollBottom){this.y--;this.scroll();}}else{this.x=this.cols-1;if(ch_width===2)// FIXME: check for xterm behavior +continue;}}row=this.y+this.ybase;// insert mode: move characters to right +if(this.insertMode){// do this twice for a fullwidth char +for(var moves=0;moves Normal Keypad (DECKPNM). +case'>':this.log('Switching back to normal keypad.');this.applicationKeypad=false;this.viewport.syncScrollArea();this.state=normal;break;default:this.state=normal;this.error('Unknown ESC control: %s.',ch);break;}break;case charset:switch(ch){case'0':// DEC Special Character and Line Drawing Set. +cs=Terminal.charsets.SCLD;break;case'A':// UK +cs=Terminal.charsets.UK;break;case'B':// United States (USASCII). +cs=Terminal.charsets.US;break;case'4':// Dutch +cs=Terminal.charsets.Dutch;break;case'C':// Finnish +case'5':cs=Terminal.charsets.Finnish;break;case'R':// French +cs=Terminal.charsets.French;break;case'Q':// FrenchCanadian +cs=Terminal.charsets.FrenchCanadian;break;case'K':// German +cs=Terminal.charsets.German;break;case'Y':// Italian +cs=Terminal.charsets.Italian;break;case'E':// NorwegianDanish +case'6':cs=Terminal.charsets.NorwegianDanish;break;case'Z':// Spanish +cs=Terminal.charsets.Spanish;break;case'H':// Swedish +case'7':cs=Terminal.charsets.Swedish;break;case'=':// Swiss +cs=Terminal.charsets.Swiss;break;case'/':// ISOLatin (actually /A) +cs=Terminal.charsets.ISOLatin;i++;break;default:// Default +cs=Terminal.charsets.US;break;}this.setgCharset(this.gcharset,cs);this.gcharset=null;this.state=normal;break;case osc:// OSC Ps ; Pt ST +// OSC Ps ; Pt BEL +// Set Text Parameters. +if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.params.push(this.currentParam);switch(this.params[0]){case 0:case 1:case 2:if(this.params[1]){this.title=this.params[1];this.handleTitle(this.title);}break;case 3:// set X property +break;case 4:case 5:// change dynamic colors +break;case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:// change dynamic ui colors +break;case 46:// change log file +break;case 50:// dynamic font +break;case 51:// emacs shell +break;case 52:// manipulate selection data +break;case 104:case 105:case 110:case 111:case 112:case 113:case 114:case 115:case 116:case 117:case 118:// reset colors +break;}this.params=[];this.currentParam=0;this.state=normal;}else{if(!this.params.length){if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;}else if(ch===';'){this.params.push(this.currentParam);this.currentParam='';}}else{this.currentParam+=ch;}}break;case csi:// '?', '>', '!' +if(ch==='?'||ch==='>'||ch==='!'){this.prefix=ch;break;}// 0 - 9 +if(ch>='0'&&ch<='9'){this.currentParam=this.currentParam*10+ch.charCodeAt(0)-48;break;}// '$', '"', ' ', '\'' +if(ch==='$'||ch==='"'||ch===' '||ch==='\''){this.postfix=ch;break;}this.params.push(this.currentParam);this.currentParam=0;// ';' +if(ch===';')break;this.state=normal;switch(ch){// CSI Ps A +// Cursor Up Ps Times (default = 1) (CUU). +case'A':this.cursorUp(this.params);break;// CSI Ps B +// Cursor Down Ps Times (default = 1) (CUD). +case'B':this.cursorDown(this.params);break;// CSI Ps C +// Cursor Forward Ps Times (default = 1) (CUF). +case'C':this.cursorForward(this.params);break;// CSI Ps D +// Cursor Backward Ps Times (default = 1) (CUB). +case'D':this.cursorBackward(this.params);break;// CSI Ps ; Ps H +// Cursor Position [row;column] (default = [1,1]) (CUP). +case'H':this.cursorPos(this.params);break;// CSI Ps J Erase in Display (ED). +case'J':this.eraseInDisplay(this.params);break;// CSI Ps K Erase in Line (EL). +case'K':this.eraseInLine(this.params);break;// CSI Pm m Character Attributes (SGR). +case'm':if(!this.prefix){this.charAttributes(this.params);}break;// CSI Ps n Device Status Report (DSR). +case'n':if(!this.prefix){this.deviceStatus(this.params);}break;/** + * Additions + */// CSI Ps @ +// Insert Ps (Blank) Character(s) (default = 1) (ICH). +case'@':this.insertChars(this.params);break;// CSI Ps E +// Cursor Next Line Ps Times (default = 1) (CNL). +case'E':this.cursorNextLine(this.params);break;// CSI Ps F +// Cursor Preceding Line Ps Times (default = 1) (CNL). +case'F':this.cursorPrecedingLine(this.params);break;// CSI Ps G +// Cursor Character Absolute [column] (default = [row,1]) (CHA). +case'G':this.cursorCharAbsolute(this.params);break;// CSI Ps L +// Insert Ps Line(s) (default = 1) (IL). +case'L':this.insertLines(this.params);break;// CSI Ps M +// Delete Ps Line(s) (default = 1) (DL). +case'M':this.deleteLines(this.params);break;// CSI Ps P +// Delete Ps Character(s) (default = 1) (DCH). +case'P':this.deleteChars(this.params);break;// CSI Ps X +// Erase Ps Character(s) (default = 1) (ECH). +case'X':this.eraseChars(this.params);break;// CSI Pm ` Character Position Absolute +// [column] (default = [row,1]) (HPA). +case'`':this.charPosAbsolute(this.params);break;// 141 61 a * HPR - +// Horizontal Position Relative +case'a':this.HPositionRelative(this.params);break;// CSI P s c +// Send Device Attributes (Primary DA). +// CSI > P s c +// Send Device Attributes (Secondary DA) +case'c':this.sendDeviceAttributes(this.params);break;// CSI Pm d +// Line Position Absolute [row] (default = [1,column]) (VPA). +case'd':this.linePosAbsolute(this.params);break;// 145 65 e * VPR - Vertical Position Relative +case'e':this.VPositionRelative(this.params);break;// CSI Ps ; Ps f +// Horizontal and Vertical Position [row;column] (default = +// [1,1]) (HVP). +case'f':this.HVPosition(this.params);break;// CSI Pm h Set Mode (SM). +// CSI ? Pm h - mouse escape codes, cursor escape codes +case'h':this.setMode(this.params);break;// CSI Pm l Reset Mode (RM). +// CSI ? Pm l +case'l':this.resetMode(this.params);break;// CSI Ps ; Ps r +// Set Scrolling Region [top;bottom] (default = full size of win- +// dow) (DECSTBM). +// CSI ? Pm r +case'r':this.setScrollRegion(this.params);break;// CSI s +// Save cursor (ANSI.SYS). +case's':this.saveCursor(this.params);break;// CSI u +// Restore cursor (ANSI.SYS). +case'u':this.restoreCursor(this.params);break;/** + * Lesser Used + */// CSI Ps I +// Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). +case'I':this.cursorForwardTab(this.params);break;// CSI Ps S Scroll up Ps lines (default = 1) (SU). +case'S':this.scrollUp(this.params);break;// CSI Ps T Scroll down Ps lines (default = 1) (SD). +// CSI Ps ; Ps ; Ps ; Ps ; Ps T +// CSI > Ps; Ps T +case'T':// if (this.prefix === '>') { +// this.resetTitleModes(this.params); +// break; +// } +// if (this.params.length > 2) { +// this.initMouseTracking(this.params); +// break; +// } +if(this.params.length<2&&!this.prefix){this.scrollDown(this.params);}break;// CSI Ps Z +// Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). +case'Z':this.cursorBackwardTab(this.params);break;// CSI Ps b Repeat the preceding graphic character Ps times (REP). +case'b':this.repeatPrecedingCharacter(this.params);break;// CSI Ps g Tab Clear (TBC). +case'g':this.tabClear(this.params);break;// CSI Pm i Media Copy (MC). +// CSI ? Pm i +// case 'i': +// this.mediaCopy(this.params); +// break; +// CSI Pm m Character Attributes (SGR). +// CSI > Ps; Ps m +// case 'm': // duplicate +// if (this.prefix === '>') { +// this.setResources(this.params); +// } else { +// this.charAttributes(this.params); +// } +// break; +// CSI Ps n Device Status Report (DSR). +// CSI > Ps n +// case 'n': // duplicate +// if (this.prefix === '>') { +// this.disableModifiers(this.params); +// } else { +// this.deviceStatus(this.params); +// } +// break; +// CSI > Ps p Set pointer mode. +// CSI ! p Soft terminal reset (DECSTR). +// CSI Ps$ p +// Request ANSI mode (DECRQM). +// CSI ? Ps$ p +// Request DEC private mode (DECRQM). +// CSI Ps ; Ps " p +case'p':switch(this.prefix){// case '>': +// this.setPointerMode(this.params); +// break; +case'!':this.softReset(this.params);break;// case '?': +// if (this.postfix === '$') { +// this.requestPrivateMode(this.params); +// } +// break; +// default: +// if (this.postfix === '"') { +// this.setConformanceLevel(this.params); +// } else if (this.postfix === '$') { +// this.requestAnsiMode(this.params); +// } +// break; +}break;// CSI Ps q Load LEDs (DECLL). +// CSI Ps SP q +// CSI Ps " q +// case 'q': +// if (this.postfix === ' ') { +// this.setCursorStyle(this.params); +// break; +// } +// if (this.postfix === '"') { +// this.setCharProtectionAttr(this.params); +// break; +// } +// this.loadLEDs(this.params); +// break; +// CSI Ps ; Ps r +// Set Scrolling Region [top;bottom] (default = full size of win- +// dow) (DECSTBM). +// CSI ? Pm r +// CSI Pt; Pl; Pb; Pr; Ps$ r +// case 'r': // duplicate +// if (this.prefix === '?') { +// this.restorePrivateValues(this.params); +// } else if (this.postfix === '$') { +// this.setAttrInRectangle(this.params); +// } else { +// this.setScrollRegion(this.params); +// } +// break; +// CSI s Save cursor (ANSI.SYS). +// CSI ? Pm s +// case 's': // duplicate +// if (this.prefix === '?') { +// this.savePrivateValues(this.params); +// } else { +// this.saveCursor(this.params); +// } +// break; +// CSI Ps ; Ps ; Ps t +// CSI Pt; Pl; Pb; Pr; Ps$ t +// CSI > Ps; Ps t +// CSI Ps SP t +// case 't': +// if (this.postfix === '$') { +// this.reverseAttrInRectangle(this.params); +// } else if (this.postfix === ' ') { +// this.setWarningBellVolume(this.params); +// } else { +// if (this.prefix === '>') { +// this.setTitleModeFeature(this.params); +// } else { +// this.manipulateWindow(this.params); +// } +// } +// break; +// CSI u Restore cursor (ANSI.SYS). +// CSI Ps SP u +// case 'u': // duplicate +// if (this.postfix === ' ') { +// this.setMarginBellVolume(this.params); +// } else { +// this.restoreCursor(this.params); +// } +// break; +// CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v +// case 'v': +// if (this.postfix === '$') { +// this.copyRectagle(this.params); +// } +// break; +// CSI Pt ; Pl ; Pb ; Pr ' w +// case 'w': +// if (this.postfix === '\'') { +// this.enableFilterRectangle(this.params); +// } +// break; +// CSI Ps x Request Terminal Parameters (DECREQTPARM). +// CSI Ps x Select Attribute Change Extent (DECSACE). +// CSI Pc; Pt; Pl; Pb; Pr$ x +// case 'x': +// if (this.postfix === '$') { +// this.fillRectangle(this.params); +// } else { +// this.requestParameters(this.params); +// //this.__(this.params); +// } +// break; +// CSI Ps ; Pu ' z +// CSI Pt; Pl; Pb; Pr$ z +// case 'z': +// if (this.postfix === '\'') { +// this.enableLocatorReporting(this.params); +// } else if (this.postfix === '$') { +// this.eraseRectangle(this.params); +// } +// break; +// CSI Pm ' { +// CSI Pt; Pl; Pb; Pr$ { +// case '{': +// if (this.postfix === '\'') { +// this.setLocatorEvents(this.params); +// } else if (this.postfix === '$') { +// this.selectiveEraseRectangle(this.params); +// } +// break; +// CSI Ps ' | +// case '|': +// if (this.postfix === '\'') { +// this.requestLocatorPosition(this.params); +// } +// break; +// CSI P m SP } +// Insert P s Column(s) (default = 1) (DECIC), VT420 and up. +// case '}': +// if (this.postfix === ' ') { +// this.insertColumns(this.params); +// } +// break; +// CSI P m SP ~ +// Delete P s Column(s) (default = 1) (DECDC), VT420 and up +// case '~': +// if (this.postfix === ' ') { +// this.deleteColumns(this.params); +// } +// break; +default:this.error('Unknown CSI code: %s.',ch);break;}this.prefix='';this.postfix='';break;case dcs:if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;switch(this.prefix){// User-Defined Keys (DECUDK). +case'':break;// Request Status String (DECRQSS). +// test: echo -e '\eP$q"p\e\\' +case'$q':var pt=this.currentParam,valid=false;switch(pt){// DECSCA +case'"q':pt='0"q';break;// DECSCL +case'"p':pt='61"p';break;// DECSTBM +case'r':pt=''+(this.scrollTop+1)+';'+(this.scrollBottom+1)+'r';break;// SGR +case'm':pt='0m';break;default:this.error('Unknown DCS Pt: %s.',pt);pt='';break;}this.send('\x1bP'+ +valid+'$r'+pt+'\x1b\\');break;// Set Termcap/Terminfo Data (xterm, experimental). +case'+p':break;// Request Termcap/Terminfo String (xterm, experimental) +// Regular xterm does not even respond to this sequence. +// This can cause a small glitch in vim. +// test: echo -ne '\eP+q6b64\e\\' +case'+q':var pt=this.currentParam,valid=false;this.send('\x1bP'+ +valid+'+r'+pt+'\x1b\\');break;default:this.error('Unknown DCS prefix: %s.',this.prefix);break;}this.currentParam=0;this.prefix='';this.state=normal;}else if(!this.currentParam){if(!this.prefix&&ch!=='$'&&ch!=='+'){this.currentParam=ch;}else if(this.prefix.length===2){this.currentParam=ch;}else{this.prefix+=ch;}}else{this.currentParam+=ch;}break;case ignore:// For PM and APC. +if(ch==='\x1b'||ch==='\x07'){if(ch==='\x1b')i++;this.state=normal;}break;}}this.updateRange(this.y);this.refresh(this.refreshStart,this.refreshEnd);};/** + * Writes text to the terminal, followed by a break line character (\n). + * @param {string} text The text to write to the terminal. + */Terminal.prototype.writeln=function(data){this.write(data+'\r\n');};/** + * Attaches a custom keydown handler which is run before keys are processed, giving consumers of + * xterm.js ultimate control as to what keys should be processed by the terminal and what keys + * should not. + * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a + * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent + * the default action. The function returns whether the event should be processed by xterm.js. + */Terminal.prototype.attachCustomKeydownHandler=function(customKeydownHandler){this.customKeydownHandler=customKeydownHandler;};/** + * Handle a keydown event + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keydown event to be handled. + */Terminal.prototype.keyDown=function(ev){// Scroll down to prompt, whenever the user presses a key. +if(this.ybase!==this.ydisp){this.scrollToBottom();}if(this.customKeydownHandler&&this.customKeydownHandler(ev)===false){return false;}if(!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)){return false;}var self=this;var result=this.evaluateKeyEscapeSequence(ev);if(result.scrollDisp){this.scrollDisp(result.scrollDisp);return this.cancel(ev,true);}if(isThirdLevelShift(this,ev)){return true;}if(result.cancel){// The event is canceled at the end already, is this necessary? +this.cancel(ev,true);}if(!result.key){return true;}this.emit('keydown',ev);this.emit('key',result.key,ev);this.showCursor();this.handler(result.key);return this.cancel(ev,true);};/** + * Returns an object that determines how a KeyboardEvent should be handled. The key of the + * returned value is the new key code to pass to the PTY. + * + * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. + */Terminal.prototype.evaluateKeyEscapeSequence=function(ev){var result={// Whether to cancel event propogation (NOTE: this may not be needed since the event is +// canceled at the end of keyDown +cancel:false,// The new key even to emit +key:undefined,// The number of characters to scroll, if this is defined it will cancel the event +scrollDisp:undefined};var modifiers=ev.shiftKey<<0|ev.altKey<<1|ev.ctrlKey<<2|ev.metaKey<<3;switch(ev.keyCode){case 8:// backspace +if(ev.shiftKey){result.key='\x08';// ^H +break;}result.key='\x7f';// ^? +break;case 9:// tab +if(ev.shiftKey){result.key='\x1b[Z';break;}result.key='\t';result.cancel=true;break;case 13:// return/enter +result.key='\r';result.cancel=true;break;case 27:// escape +result.key='\x1b';result.cancel=true;break;case 37:// left-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'D';// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3D'){result.key='\x1b[1;5D';}}else if(this.applicationCursor){result.key='\x1bOD';}else{result.key='\x1b[D';}break;case 39:// right-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'C';// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3C'){result.key='\x1b[1;5C';}}else if(this.applicationCursor){result.key='\x1bOC';}else{result.key='\x1b[C';}break;case 38:// up-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'A';// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3A'){result.key='\x1b[1;5A';}}else if(this.applicationCursor){result.key='\x1bOA';}else{result.key='\x1b[A';}break;case 40:// down-arrow +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'B';// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow +// http://unix.stackexchange.com/a/108106 +if(result.key=='\x1b[1;3B'){result.key='\x1b[1;5B';}}else if(this.applicationCursor){result.key='\x1bOB';}else{result.key='\x1b[B';}break;case 45:// insert +if(!ev.shiftKey&&!ev.ctrlKey){// or + are used to +// copy-paste on some systems. +result.key='\x1b[2~';}break;case 46:// delete +if(modifiers){result.key='\x1b[3;'+(modifiers+1)+'~';}else{result.key='\x1b[3~';}break;case 36:// home +if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'H';else if(this.applicationCursor)result.key='\x1bOH';else result.key='\x1b[H';break;case 35:// end +if(modifiers)result.key='\x1b[1;'+(modifiers+1)+'F';else if(this.applicationCursor)result.key='\x1bOF';else result.key='\x1b[F';break;case 33:// page up +if(ev.shiftKey){result.scrollDisp=-(this.rows-1);}else{result.key='\x1b[5~';}break;case 34:// page down +if(ev.shiftKey){result.scrollDisp=this.rows-1;}else{result.key='\x1b[6~';}break;case 112:// F1-F12 +if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'P';}else{result.key='\x1bOP';}break;case 113:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'Q';}else{result.key='\x1bOQ';}break;case 114:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'R';}else{result.key='\x1bOR';}break;case 115:if(modifiers){result.key='\x1b[1;'+(modifiers+1)+'S';}else{result.key='\x1bOS';}break;case 116:if(modifiers){result.key='\x1b[15;'+(modifiers+1)+'~';}else{result.key='\x1b[15~';}break;case 117:if(modifiers){result.key='\x1b[17;'+(modifiers+1)+'~';}else{result.key='\x1b[17~';}break;case 118:if(modifiers){result.key='\x1b[18;'+(modifiers+1)+'~';}else{result.key='\x1b[18~';}break;case 119:if(modifiers){result.key='\x1b[19;'+(modifiers+1)+'~';}else{result.key='\x1b[19~';}break;case 120:if(modifiers){result.key='\x1b[20;'+(modifiers+1)+'~';}else{result.key='\x1b[20~';}break;case 121:if(modifiers){result.key='\x1b[21;'+(modifiers+1)+'~';}else{result.key='\x1b[21~';}break;case 122:if(modifiers){result.key='\x1b[23;'+(modifiers+1)+'~';}else{result.key='\x1b[23~';}break;case 123:if(modifiers){result.key='\x1b[24;'+(modifiers+1)+'~';}else{result.key='\x1b[24~';}break;default:// a-z and space +if(ev.ctrlKey&&!ev.shiftKey&&!ev.altKey&&!ev.metaKey){if(ev.keyCode>=65&&ev.keyCode<=90){result.key=String.fromCharCode(ev.keyCode-64);}else if(ev.keyCode===32){// NUL +result.key=String.fromCharCode(0);}else if(ev.keyCode>=51&&ev.keyCode<=55){// escape, file sep, group sep, record sep, unit sep +result.key=String.fromCharCode(ev.keyCode-51+27);}else if(ev.keyCode===56){// delete +result.key=String.fromCharCode(127);}else if(ev.keyCode===219){// ^[ - escape +result.key=String.fromCharCode(27);}else if(ev.keyCode===221){// ^] - group sep +result.key=String.fromCharCode(29);}}else if(!this.browser.isMac&&ev.altKey&&!ev.ctrlKey&&!ev.metaKey){// On Mac this is a third level shift. Use instead. +if(ev.keyCode>=65&&ev.keyCode<=90){result.key='\x1b'+String.fromCharCode(ev.keyCode+32);}else if(ev.keyCode===192){result.key='\x1b`';}else if(ev.keyCode>=48&&ev.keyCode<=57){result.key='\x1b'+(ev.keyCode-48);}}break;}return result;};/** + * Set the G level of the terminal + * @param g + */Terminal.prototype.setgLevel=function(g){this.glevel=g;this.charset=this.charsets[g];};/** + * Set the charset for the given G level of the terminal + * @param g + * @param charset + */Terminal.prototype.setgCharset=function(g,charset){this.charsets[g]=charset;if(this.glevel===g){this.charset=charset;}};/** + * Handle a keypress event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keypress event to be handled. + */Terminal.prototype.keyPress=function(ev){var key;this.cancel(ev);if(ev.charCode){key=ev.charCode;}else if(ev.which==null){key=ev.keyCode;}else if(ev.which!==0&&ev.charCode!==0){key=ev.which;}else{return false;}if(!key||(ev.altKey||ev.ctrlKey||ev.metaKey)&&!isThirdLevelShift(this,ev)){return false;}key=String.fromCharCode(key);this.emit('keypress',key,ev);this.emit('key',key,ev);this.showCursor();this.handler(key);return false;};/** + * Send data for handling to the terminal + * @param {string} data + */Terminal.prototype.send=function(data){var self=this;if(!this.queue){setTimeout(function(){self.handler(self.queue);self.queue='';},1);}this.queue+=data;};/** + * Ring the bell. + * Note: We could do sweet things with webaudio here + */Terminal.prototype.bell=function(){if(!this.visualBell)return;var self=this;this.element.style.borderColor='white';setTimeout(function(){self.element.style.borderColor='';},10);if(this.popOnBell)this.focus();};/** + * Log the current state to the console. + */Terminal.prototype.log=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.log)return;var args=Array.prototype.slice.call(arguments);this.context.console.log.apply(this.context.console,args);};/** + * Log the current state as error to the console. + */Terminal.prototype.error=function(){if(!this.debug)return;if(!this.context.console||!this.context.console.error)return;var args=Array.prototype.slice.call(arguments);this.context.console.error.apply(this.context.console,args);};/** + * Resizes the terminal. + * + * @param {number} x The number of columns to resize to. + * @param {number} y The number of rows to resize to. + */Terminal.prototype.resize=function(x,y){var line,el,i,j,ch,addToY;if(x===this.cols&&y===this.rows){return;}if(x<1)x=1;if(y<1)y=1;// resize cols +j=this.cols;if(j x) +i=this.lines.length;while(i--){while(this.lines[i].length>x){this.lines[i].pop();}}}this.setupStops(j);this.cols=x;// resize rows +j=this.rows;addToY=0;if(j0&&this.lines.length<=this.ybase+this.y+addToY+1){// There is room above the buffer and there are no empty elements below the line, +// scroll up +this.ybase--;addToY++;if(this.ydisp>0){// Viewport is at the top of the buffer, must increase downwards +this.ydisp--;}}else{// Add a blank line if there is no buffer left at the top to scroll to, or if there +// are blank lines after the cursor +this.lines.push(this.blankLine());}}if(this.children.length y) +while(j-->y){if(this.lines.length>y+this.ybase){if(this.lines.length>this.ybase+this.y+1){// The line is a blank line below the cursor, remove it +this.lines.pop();}else{// The line is the cursor, scroll down +this.ybase++;this.ydisp++;}}if(this.children.length>y){el=this.children.shift();if(!el)continue;el.parentNode.removeChild(el);}}}this.rows=y;// Make sure that the cursor stays on screen +if(this.y>=y){this.y=y-1;}if(addToY){this.y+=addToY;}if(this.x>=x){this.x=x-1;}this.scrollTop=0;this.scrollBottom=y-1;this.refresh(0,this.rows-1);this.normal=null;this.geometry=[this.cols,this.rows];this.emit('resize',{terminal:this,cols:x,rows:y});};/** + * Updates the range of rows to refresh + * @param {number} y The number of rows to refresh next. + */Terminal.prototype.updateRange=function(y){if(ythis.refreshEnd)this.refreshEnd=y;// if (y > this.refreshEnd) { +// this.refreshEnd = y; +// if (y > this.rows - 1) { +// this.refreshEnd = this.rows - 1; +// } +// } +};/** + * Set the range of refreshing to the maximum value + */Terminal.prototype.maxRange=function(){this.refreshStart=0;this.refreshEnd=this.rows-1;};/** + * Setup the tab stops. + * @param {number} i + */Terminal.prototype.setupStops=function(i){if(i!=null){if(!this.tabs[i]){i=this.prevStop(i);}}else{this.tabs={};i=0;}for(;i0){}return x>=this.cols?this.cols-1:x<0?0:x;};/** + * Move the cursor one tab stop forward from the given position (default is current). + * @param {number} x The position to move the cursor one tab stop forward. + */Terminal.prototype.nextStop=function(x){if(x==null)x=this.x;while(!this.tabs[++x]&&x=this.cols?this.cols-1:x<0?0:x;};/** + * Erase in the identified line everything from "x" to the end of the line (right). + * @param {number} x The column from which to start erasing to the end of the line. + * @param {number} y The line in which to operate. + */Terminal.prototype.eraseRight=function(x,y){var line=this.lines[this.ybase+y],ch=[this.eraseAttr(),' ',1];// xterm +for(;xthis.scrollBottom){this.y--;this.scroll();}this.state=normal;};/** + * ESC M Reverse Index (RI is 0x8d). + */Terminal.prototype.reverseIndex=function(){var j;this.y--;if(this.y=this.rows){this.y=this.rows-1;}};/** + * CSI Ps C + * Cursor Forward Ps Times (default = 1) (CUF). + */Terminal.prototype.cursorForward=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Ps D + * Cursor Backward Ps Times (default = 1) (CUB). + */Terminal.prototype.cursorBackward=function(params){var param=params[0];if(param<1)param=1;this.x-=param;if(this.x<0)this.x=0;};/** + * CSI Ps ; Ps H + * Cursor Position [row;column] (default = [1,1]) (CUP). + */Terminal.prototype.cursorPos=function(params){var row,col;row=params[0]-1;if(params.length>=2){col=params[1]-1;}else{col=0;}if(row<0){row=0;}else if(row>=this.rows){row=this.rows-1;}if(col<0){col=0;}else if(col>=this.cols){col=this.cols-1;}this.x=col;this.y=row;};/** + * CSI Ps J Erase in Display (ED). + * Ps = 0 -> Erase Below (default). + * Ps = 1 -> Erase Above. + * Ps = 2 -> Erase All. + * Ps = 3 -> Erase Saved Lines (xterm). + * CSI ? Ps J + * Erase in Display (DECSED). + * Ps = 0 -> Selective Erase Below (default). + * Ps = 1 -> Selective Erase Above. + * Ps = 2 -> Selective Erase All. + */Terminal.prototype.eraseInDisplay=function(params){var j;switch(params[0]){case 0:this.eraseRight(this.x,this.y);j=this.y+1;for(;j Erase to Right (default). + * Ps = 1 -> Erase to Left. + * Ps = 2 -> Erase All. + * CSI ? Ps K + * Erase in Line (DECSEL). + * Ps = 0 -> Selective Erase to Right (default). + * Ps = 1 -> Selective Erase to Left. + * Ps = 2 -> Selective Erase All. + */Terminal.prototype.eraseInLine=function(params){switch(params[0]){case 0:this.eraseRight(this.x,this.y);break;case 1:this.eraseLeft(this.x,this.y);break;case 2:this.eraseLine(this.y);break;}};/** + * CSI Pm m Character Attributes (SGR). + * Ps = 0 -> Normal (default). + * Ps = 1 -> Bold. + * Ps = 4 -> Underlined. + * Ps = 5 -> Blink (appears as Bold). + * Ps = 7 -> Inverse. + * Ps = 8 -> Invisible, i.e., hidden (VT300). + * Ps = 2 2 -> Normal (neither bold nor faint). + * Ps = 2 4 -> Not underlined. + * Ps = 2 5 -> Steady (not blinking). + * Ps = 2 7 -> Positive (not inverse). + * Ps = 2 8 -> Visible, i.e., not hidden (VT300). + * Ps = 3 0 -> Set foreground color to Black. + * Ps = 3 1 -> Set foreground color to Red. + * Ps = 3 2 -> Set foreground color to Green. + * Ps = 3 3 -> Set foreground color to Yellow. + * Ps = 3 4 -> Set foreground color to Blue. + * Ps = 3 5 -> Set foreground color to Magenta. + * Ps = 3 6 -> Set foreground color to Cyan. + * Ps = 3 7 -> Set foreground color to White. + * Ps = 3 9 -> Set foreground color to default (original). + * Ps = 4 0 -> Set background color to Black. + * Ps = 4 1 -> Set background color to Red. + * Ps = 4 2 -> Set background color to Green. + * Ps = 4 3 -> Set background color to Yellow. + * Ps = 4 4 -> Set background color to Blue. + * Ps = 4 5 -> Set background color to Magenta. + * Ps = 4 6 -> Set background color to Cyan. + * Ps = 4 7 -> Set background color to White. + * Ps = 4 9 -> Set background color to default (original). + * + * If 16-color support is compiled, the following apply. Assume + * that xterm's resources are set so that the ISO color codes are + * the first 8 of a set of 16. Then the aixterm colors are the + * bright versions of the ISO colors: + * Ps = 9 0 -> Set foreground color to Black. + * Ps = 9 1 -> Set foreground color to Red. + * Ps = 9 2 -> Set foreground color to Green. + * Ps = 9 3 -> Set foreground color to Yellow. + * Ps = 9 4 -> Set foreground color to Blue. + * Ps = 9 5 -> Set foreground color to Magenta. + * Ps = 9 6 -> Set foreground color to Cyan. + * Ps = 9 7 -> Set foreground color to White. + * Ps = 1 0 0 -> Set background color to Black. + * Ps = 1 0 1 -> Set background color to Red. + * Ps = 1 0 2 -> Set background color to Green. + * Ps = 1 0 3 -> Set background color to Yellow. + * Ps = 1 0 4 -> Set background color to Blue. + * Ps = 1 0 5 -> Set background color to Magenta. + * Ps = 1 0 6 -> Set background color to Cyan. + * Ps = 1 0 7 -> Set background color to White. + * + * If xterm is compiled with the 16-color support disabled, it + * supports the following, from rxvt: + * Ps = 1 0 0 -> Set foreground and background color to + * default. + * + * If 88- or 256-color support is compiled, the following apply. + * Ps = 3 8 ; 5 ; Ps -> Set foreground color to the second + * Ps. + * Ps = 4 8 ; 5 ; Ps -> Set background color to the second + * Ps. + */Terminal.prototype.charAttributes=function(params){// Optimize a single SGR0. +if(params.length===1&¶ms[0]===0){this.curAttr=this.defAttr;return;}var l=params.length,i=0,flags=this.curAttr>>18,fg=this.curAttr>>9&0x1ff,bg=this.curAttr&0x1ff,p;for(;i=30&&p<=37){// fg color 8 +fg=p-30;}else if(p>=40&&p<=47){// bg color 8 +bg=p-40;}else if(p>=90&&p<=97){// fg color 16 +p+=8;fg=p-90;}else if(p>=100&&p<=107){// bg color 16 +p+=8;bg=p-100;}else if(p===0){// default +flags=this.defAttr>>18;fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;// flags = 0; +// fg = 0x1ff; +// bg = 0x1ff; +}else if(p===1){// bold text +flags|=1;}else if(p===4){// underlined text +flags|=2;}else if(p===5){// blink +flags|=4;}else if(p===7){// inverse and positive +// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' +flags|=8;}else if(p===8){// invisible +flags|=16;}else if(p===22){// not bold +flags&=~1;}else if(p===24){// not underlined +flags&=~2;}else if(p===25){// not blink +flags&=~4;}else if(p===27){// not inverse +flags&=~8;}else if(p===28){// not invisible +flags&=~16;}else if(p===39){// reset fg +fg=this.defAttr>>9&0x1ff;}else if(p===49){// reset bg +bg=this.defAttr&0x1ff;}else if(p===38){// fg color 256 +if(params[i+1]===2){i+=2;fg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(fg===-1)fg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;fg=p;}}else if(p===48){// bg color 256 +if(params[i+1]===2){i+=2;bg=matchColor(params[i]&0xff,params[i+1]&0xff,params[i+2]&0xff);if(bg===-1)bg=0x1ff;i+=2;}else if(params[i+1]===5){i+=2;p=params[i]&0xff;bg=p;}}else if(p===100){// reset fg/bg +fg=this.defAttr>>9&0x1ff;bg=this.defAttr&0x1ff;}else{this.error('Unknown SGR attribute: %d.',p);}}this.curAttr=flags<<18|fg<<9|bg;};/** + * CSI Ps n Device Status Report (DSR). + * Ps = 5 -> Status Report. Result (``OK'') is + * CSI 0 n + * Ps = 6 -> Report Cursor Position (CPR) [row;column]. + * Result is + * CSI r ; c R + * CSI ? Ps n + * Device Status Report (DSR, DEC-specific). + * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI + * ? r ; c R (assumes page is zero). + * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). + * or CSI ? 1 1 n (not ready). + * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) + * or CSI ? 2 1 n (locked). + * Ps = 2 6 -> Report Keyboard status as + * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). + * The last two parameters apply to VT400 & up, and denote key- + * board ready and LK01 respectively. + * Ps = 5 3 -> Report Locator status as + * CSI ? 5 3 n Locator available, if compiled-in, or + * CSI ? 5 0 n No Locator, if not. + */Terminal.prototype.deviceStatus=function(params){if(!this.prefix){switch(params[0]){case 5:// status report +this.send('\x1b[0n');break;case 6:// cursor position +this.send('\x1b['+(this.y+1)+';'+(this.x+1)+'R');break;}}else if(this.prefix==='?'){// modern xterm doesnt seem to +// respond to any of these except ?6, 6, and 5 +switch(params[0]){case 6:// cursor position +this.send('\x1b[?'+(this.y+1)+';'+(this.x+1)+'R');break;case 15:// no printer +// this.send('\x1b[?11n'); +break;case 25:// dont support user defined keys +// this.send('\x1b[?21n'); +break;case 26:// north american keyboard +// this.send('\x1b[?27;1;0;0n'); +break;case 53:// no dec locator/mouse +// this.send('\x1b[?50n'); +break;}}};/** + * Additions + *//** + * CSI Ps @ + * Insert Ps (Blank) Character(s) (default = 1) (ICH). + */Terminal.prototype.insertChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm +while(param--&&j=this.rows){this.y=this.rows-1;}this.x=0;};/** + * CSI Ps F + * Cursor Preceding Line Ps Times (default = 1) (CNL). + * reuse CSI Ps A ? + */Terminal.prototype.cursorPrecedingLine=function(params){var param=params[0];if(param<1)param=1;this.y-=param;if(this.y<0)this.y=0;this.x=0;};/** + * CSI Ps G + * Cursor Character Absolute [column] (default = [row,1]) (CHA). + */Terminal.prototype.cursorCharAbsolute=function(params){var param=params[0];if(param<1)param=1;this.x=param-1;};/** + * CSI Ps L + * Insert Ps Line(s) (default = 1) (IL). + */Terminal.prototype.insertLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j+1;while(param--){// test: echo -e '\e[44m\e[1L\e[0m' +// blankLine(true) - xterm/linux behavior +this.lines.splice(row,0,this.blankLine(true));this.lines.splice(j,1);}// this.maxRange(); +this.updateRange(this.y);this.updateRange(this.scrollBottom);};/** + * CSI Ps M + * Delete Ps Line(s) (default = 1) (DL). + */Terminal.prototype.deleteLines=function(params){var param,row,j;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.rows-1-this.scrollBottom;j=this.rows-1+this.ybase-j;while(param--){// test: echo -e '\e[44m\e[1M\e[0m' +// blankLine(true) - xterm/linux behavior +this.lines.splice(j+1,0,this.blankLine(true));this.lines.splice(row,1);}// this.maxRange(); +this.updateRange(this.y);this.updateRange(this.scrollBottom);};/** + * CSI Ps P + * Delete Ps Character(s) (default = 1) (DCH). + */Terminal.prototype.deleteChars=function(params){var param,row,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;ch=[this.eraseAttr(),' ',1];// xterm +while(param--){this.lines[row].splice(this.x,1);this.lines[row].push(ch);}};/** + * CSI Ps X + * Erase Ps Character(s) (default = 1) (ECH). + */Terminal.prototype.eraseChars=function(params){var param,row,j,ch;param=params[0];if(param<1)param=1;row=this.y+this.ybase;j=this.x;ch=[this.eraseAttr(),' ',1];// xterm +while(param--&&j=this.cols){this.x=this.cols-1;}};/** + * 141 61 a * HPR - + * Horizontal Position Relative + * reuse CSI Ps C ? + */Terminal.prototype.HPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.x+=param;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Ps c Send Device Attributes (Primary DA). + * Ps = 0 or omitted -> request attributes from terminal. The + * response depends on the decTerminalID resource setting. + * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'') + * -> CSI ? 1 ; 0 c (``VT101 with No Options'') + * -> CSI ? 6 c (``VT102'') + * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'') + * The VT100-style response parameters do not mean anything by + * themselves. VT220 parameters do, telling the host what fea- + * tures the terminal supports: + * Ps = 1 -> 132-columns. + * Ps = 2 -> Printer. + * Ps = 6 -> Selective erase. + * Ps = 8 -> User-defined keys. + * Ps = 9 -> National replacement character sets. + * Ps = 1 5 -> Technical characters. + * Ps = 2 2 -> ANSI color, e.g., VT525. + * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode). + * CSI > Ps c + * Send Device Attributes (Secondary DA). + * Ps = 0 or omitted -> request the terminal's identification + * code. The response depends on the decTerminalID resource set- + * ting. It should apply only to VT220 and up, but xterm extends + * this to VT100. + * -> CSI > Pp ; Pv ; Pc c + * where Pp denotes the terminal type + * Pp = 0 -> ``VT100''. + * Pp = 1 -> ``VT220''. + * and Pv is the firmware version (for xterm, this was originally + * the XFree86 patch number, starting with 95). In a DEC termi- + * nal, Pc indicates the ROM cartridge registration number and is + * always zero. + * More information: + * xterm/charproc.c - line 2012, for more information. + * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) + */Terminal.prototype.sendDeviceAttributes=function(params){if(params[0]>0)return;if(!this.prefix){if(this.is('xterm')||this.is('rxvt-unicode')||this.is('screen')){this.send('\x1b[?1;2c');}else if(this.is('linux')){this.send('\x1b[?6c');}}else if(this.prefix==='>'){// xterm and urxvt +// seem to spit this +// out around ~370 times (?). +if(this.is('xterm')){this.send('\x1b[>0;276;0c');}else if(this.is('rxvt-unicode')){this.send('\x1b[>85;95;0c');}else if(this.is('linux')){// not supported by linux console. +// linux console echoes parameters. +this.send(params[0]+'c');}else if(this.is('screen')){this.send('\x1b[>83;40003;0c');}}};/** + * CSI Pm d + * Line Position Absolute [row] (default = [1,column]) (VPA). + */Terminal.prototype.linePosAbsolute=function(params){var param=params[0];if(param<1)param=1;this.y=param-1;if(this.y>=this.rows){this.y=this.rows-1;}};/** + * 145 65 e * VPR - Vertical Position Relative + * reuse CSI Ps B ? + */Terminal.prototype.VPositionRelative=function(params){var param=params[0];if(param<1)param=1;this.y+=param;if(this.y>=this.rows){this.y=this.rows-1;}};/** + * CSI Ps ; Ps f + * Horizontal and Vertical Position [row;column] (default = + * [1,1]) (HVP). + */Terminal.prototype.HVPosition=function(params){if(params[0]<1)params[0]=1;if(params[1]<1)params[1]=1;this.y=params[0]-1;if(this.y>=this.rows){this.y=this.rows-1;}this.x=params[1]-1;if(this.x>=this.cols){this.x=this.cols-1;}};/** + * CSI Pm h Set Mode (SM). + * Ps = 2 -> Keyboard Action Mode (AM). + * Ps = 4 -> Insert Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Automatic Newline (LNM). + * CSI ? Pm h + * DEC Private Mode Set (DECSET). + * Ps = 1 -> Application Cursor Keys (DECCKM). + * Ps = 2 -> Designate USASCII for character sets G0-G3 + * (DECANM), and set VT100 mode. + * Ps = 3 -> 132 Column Mode (DECCOLM). + * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM). + * Ps = 5 -> Reverse Video (DECSCNM). + * Ps = 6 -> Origin Mode (DECOM). + * Ps = 7 -> Wraparound Mode (DECAWM). + * Ps = 8 -> Auto-repeat Keys (DECARM). + * Ps = 9 -> Send Mouse X & Y on button press. See the sec- + * tion Mouse Tracking. + * Ps = 1 0 -> Show toolbar (rxvt). + * Ps = 1 2 -> Start Blinking Cursor (att610). + * Ps = 1 8 -> Print form feed (DECPFF). + * Ps = 1 9 -> Set print extent to full screen (DECPEX). + * Ps = 2 5 -> Show Cursor (DECTCEM). + * Ps = 3 0 -> Show scrollbar (rxvt). + * Ps = 3 5 -> Enable font-shifting functions (rxvt). + * Ps = 3 8 -> Enter Tektronix Mode (DECTEK). + * Ps = 4 0 -> Allow 80 -> 132 Mode. + * Ps = 4 1 -> more(1) fix (see curses resource). + * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN- + * RCM). + * Ps = 4 4 -> Turn On Margin Bell. + * Ps = 4 5 -> Reverse-wraparound Mode. + * Ps = 4 6 -> Start Logging. This is normally disabled by a + * compile-time option. + * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 6 6 -> Application keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends backspace (DECBKM). + * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Enable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt). + * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit. + * (enables the eightBitInput resource). + * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num- + * Lock keys. (This enables the numLock resource). + * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This + * enables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete + * key. + * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This + * enables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Keep selection even if not highlighted. + * (This enables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Enable Urgency window manager hint when + * Control-G is received. (This enables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Enable raising of the window when Control-G + * is received. (enables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate + * Screen Buffer, clearing it first. (This may be disabled by + * the titeInhibit resource). This combines the effects of the 1 + * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based + * applications rather than the 4 7 mode. + * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Set Sun function-key mode. + * Ps = 1 0 5 2 -> Set HP function-key mode. + * Ps = 1 0 5 3 -> Set SCO function-key mode. + * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Set VT220 keyboard emulation. + * Ps = 2 0 0 4 -> Set bracketed paste mode. + * Modes: + * http: *vt100.net/docs/vt220-rm/chapter4.html + */Terminal.prototype.setMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i1000;this.mouseEvents=true;this.element.style.cursor='default';this.log('Binding to mouse events.');break;case 1004:// send focusin/focusout events +// focusin: ^[[I +// focusout: ^[[O +this.sendFocus=true;break;case 1005:// utf8 ext mode mouse +this.utfMouse=true;// for wide terminals +// simply encodes large values as utf8 characters +break;case 1006:// sgr ext mode mouse +this.sgrMouse=true;// for wide terminals +// does not add 32 to fields +// press: ^[[ Keyboard Action Mode (AM). + * Ps = 4 -> Replace Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Normal Linefeed (LNM). + * CSI ? Pm l + * DEC Private Mode Reset (DECRST). + * Ps = 1 -> Normal Cursor Keys (DECCKM). + * Ps = 2 -> Designate VT52 mode (DECANM). + * Ps = 3 -> 80 Column Mode (DECCOLM). + * Ps = 4 -> Jump (Fast) Scroll (DECSCLM). + * Ps = 5 -> Normal Video (DECSCNM). + * Ps = 6 -> Normal Cursor Mode (DECOM). + * Ps = 7 -> No Wraparound Mode (DECAWM). + * Ps = 8 -> No Auto-repeat Keys (DECARM). + * Ps = 9 -> Don't send Mouse X & Y on button press. + * Ps = 1 0 -> Hide toolbar (rxvt). + * Ps = 1 2 -> Stop Blinking Cursor (att610). + * Ps = 1 8 -> Don't print form feed (DECPFF). + * Ps = 1 9 -> Limit print to scrolling region (DECPEX). + * Ps = 2 5 -> Hide Cursor (DECTCEM). + * Ps = 3 0 -> Don't show scrollbar (rxvt). + * Ps = 3 5 -> Disable font-shifting functions (rxvt). + * Ps = 4 0 -> Disallow 80 -> 132 Mode. + * Ps = 4 1 -> No more(1) fix (see curses resource). + * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC- + * NRCM). + * Ps = 4 4 -> Turn Off Margin Bell. + * Ps = 4 5 -> No Reverse-wraparound Mode. + * Ps = 4 6 -> Stop Logging. (This is normally disabled by a + * compile-time option). + * Ps = 4 7 -> Use Normal Screen Buffer. + * Ps = 6 6 -> Numeric keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends delete (DECBKM). + * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Disable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output + * (rxvt). + * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables + * the eightBitInput resource). + * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num- + * Lock keys. (This disables the numLock resource). + * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key. + * (This disables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad + * Delete key. + * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key. + * (This disables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Do not keep selection when not highlighted. + * (This disables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Disable Urgency window manager hint when + * Control-G is received. (This disables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Disable raising of the window when Control- + * G is received. (This disables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen + * first if in the Alternate Screen. (This may be disabled by + * the titeInhibit resource). + * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor + * as in DECRC. (This may be disabled by the titeInhibit + * resource). This combines the effects of the 1 0 4 7 and 1 0 + * 4 8 modes. Use this with terminfo-based applications rather + * than the 4 7 mode. + * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Reset Sun function-key mode. + * Ps = 1 0 5 2 -> Reset HP function-key mode. + * Ps = 1 0 5 3 -> Reset SCO function-key mode. + * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. + * Ps = 2 0 0 4 -> Reset bracketed paste mode. + */Terminal.prototype.resetMode=function(params){if((typeof params==='undefined'?'undefined':_typeof(params))==='object'){var l=params.length,i=0;for(;i Ps; Ps T + * Reset one or more features of the title modes to the default + * value. Normally, "reset" disables the feature. It is possi- + * ble to disable the ability to reset features by compiling a + * different default for the title modes into xterm. + * Ps = 0 -> Do not set window/icon labels using hexadecimal. + * Ps = 1 -> Do not query window/icon labels using hexadeci- + * mal. + * Ps = 2 -> Do not set window/icon labels using UTF-8. + * Ps = 3 -> Do not query window/icon labels using UTF-8. + * (See discussion of "Title Modes"). + */Terminal.prototype.resetTitleModes=function(params){;};/** + * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + */Terminal.prototype.cursorBackwardTab=function(params){var param=params[0]||1;while(param--){this.x=this.prevStop();}};/** + * CSI Ps b Repeat the preceding graphic character Ps times (REP). + */Terminal.prototype.repeatPrecedingCharacter=function(params){var param=params[0]||1,line=this.lines[this.ybase+this.y],ch=line[this.x-1]||[this.defAttr,' ',1];while(param--){line[this.x++]=ch;}};/** + * CSI Ps g Tab Clear (TBC). + * Ps = 0 -> Clear Current Column (default). + * Ps = 3 -> Clear All. + * Potentially: + * Ps = 2 -> Clear Stops on Line. + * http://vt100.net/annarbor/aaa-ug/section6.html + */Terminal.prototype.tabClear=function(params){var param=params[0];if(param<=0){delete this.tabs[this.x];}else if(param===3){this.tabs={};}};/** + * CSI Pm i Media Copy (MC). + * Ps = 0 -> Print screen (default). + * Ps = 4 -> Turn off printer controller mode. + * Ps = 5 -> Turn on printer controller mode. + * CSI ? Pm i + * Media Copy (MC, DEC-specific). + * Ps = 1 -> Print line containing cursor. + * Ps = 4 -> Turn off autoprint mode. + * Ps = 5 -> Turn on autoprint mode. + * Ps = 1 0 -> Print composed display, ignores DECPEX. + * Ps = 1 1 -> Print all pages. + */Terminal.prototype.mediaCopy=function(params){;};/** + * CSI > Ps; Ps m + * Set or reset resource-values used by xterm to decide whether + * to construct escape sequences holding information about the + * modifiers pressed with a given key. The first parameter iden- + * tifies the resource to set/reset. The second parameter is the + * value to assign to the resource. If the second parameter is + * omitted, the resource is reset to its initial value. + * Ps = 1 -> modifyCursorKeys. + * Ps = 2 -> modifyFunctionKeys. + * Ps = 4 -> modifyOtherKeys. + * If no parameters are given, all resources are reset to their + * initial values. + */Terminal.prototype.setResources=function(params){;};/** + * CSI > Ps n + * Disable modifiers which may be enabled via the CSI > Ps; Ps m + * sequence. This corresponds to a resource value of "-1", which + * cannot be set with the other sequence. The parameter identi- + * fies the resource to be disabled: + * Ps = 1 -> modifyCursorKeys. + * Ps = 2 -> modifyFunctionKeys. + * Ps = 4 -> modifyOtherKeys. + * If the parameter is omitted, modifyFunctionKeys is disabled. + * When modifyFunctionKeys is disabled, xterm uses the modifier + * keys to make an extended sequence of functions rather than + * adding a parameter to each function key to denote the modi- + * fiers. + */Terminal.prototype.disableModifiers=function(params){;};/** + * CSI > Ps p + * Set resource value pointerMode. This is used by xterm to + * decide whether to hide the pointer cursor as the user types. + * Valid values for the parameter: + * Ps = 0 -> never hide the pointer. + * Ps = 1 -> hide if the mouse tracking mode is not enabled. + * Ps = 2 -> always hide the pointer. If no parameter is + * given, xterm uses the default, which is 1 . + */Terminal.prototype.setPointerMode=function(params){;};/** + * CSI ! p Soft terminal reset (DECSTR). + * http://vt100.net/docs/vt220-rm/table4-10.html + */Terminal.prototype.softReset=function(params){this.cursorHidden=false;this.insertMode=false;this.originMode=false;this.wraparoundMode=false;// autowrap +this.applicationKeypad=false;// ? +this.viewport.syncScrollArea();this.applicationCursor=false;this.scrollTop=0;this.scrollBottom=this.rows-1;this.curAttr=this.defAttr;this.x=this.y=0;// ? +this.charset=null;this.glevel=0;// ?? +this.charsets=[null];// ?? +};/** + * CSI Ps$ p + * Request ANSI mode (DECRQM). For VT300 and up, reply is + * CSI Ps; Pm$ y + * where Ps is the mode number as in RM, and Pm is the mode + * value: + * 0 - not recognized + * 1 - set + * 2 - reset + * 3 - permanently set + * 4 - permanently reset + */Terminal.prototype.requestAnsiMode=function(params){;};/** + * CSI ? Ps$ p + * Request DEC private mode (DECRQM). For VT300 and up, reply is + * CSI ? Ps; Pm$ p + * where Ps is the mode number as in DECSET, Pm is the mode value + * as in the ANSI DECRQM. + */Terminal.prototype.requestPrivateMode=function(params){;};/** + * CSI Ps ; Ps " p + * Set conformance level (DECSCL). Valid values for the first + * parameter: + * Ps = 6 1 -> VT100. + * Ps = 6 2 -> VT200. + * Ps = 6 3 -> VT300. + * Valid values for the second parameter: + * Ps = 0 -> 8-bit controls. + * Ps = 1 -> 7-bit controls (always set for VT100). + * Ps = 2 -> 8-bit controls. + */Terminal.prototype.setConformanceLevel=function(params){;};/** + * CSI Ps q Load LEDs (DECLL). + * Ps = 0 -> Clear all LEDS (default). + * Ps = 1 -> Light Num Lock. + * Ps = 2 -> Light Caps Lock. + * Ps = 3 -> Light Scroll Lock. + * Ps = 2 1 -> Extinguish Num Lock. + * Ps = 2 2 -> Extinguish Caps Lock. + * Ps = 2 3 -> Extinguish Scroll Lock. + */Terminal.prototype.loadLEDs=function(params){;};/** + * CSI Ps SP q + * Set cursor style (DECSCUSR, VT520). + * Ps = 0 -> blinking block. + * Ps = 1 -> blinking block (default). + * Ps = 2 -> steady block. + * Ps = 3 -> blinking underline. + * Ps = 4 -> steady underline. + */Terminal.prototype.setCursorStyle=function(params){;};/** + * CSI Ps " q + * Select character protection attribute (DECSCA). Valid values + * for the parameter: + * Ps = 0 -> DECSED and DECSEL can erase (default). + * Ps = 1 -> DECSED and DECSEL cannot erase. + * Ps = 2 -> DECSED and DECSEL can erase. + */Terminal.prototype.setCharProtectionAttr=function(params){;};/** + * CSI ? Pm r + * Restore DEC Private Mode Values. The value of Ps previously + * saved is restored. Ps values are the same as for DECSET. + */Terminal.prototype.restorePrivateValues=function(params){;};/** + * CSI Pt; Pl; Pb; Pr; Ps$ r + * Change Attributes in Rectangular Area (DECCARA), VT400 and up. + * Pt; Pl; Pb; Pr denotes the rectangle. + * Ps denotes the SGR attributes to change: 0, 1, 4, 5, 7. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.setAttrInRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3],attr=params[4];var line,i;for(;t Locator disabled (default). + * Ps = 1 -> Locator enabled. + * Ps = 2 -> Locator enabled for one report, then disabled. + * The second parameter specifies the coordinate unit for locator + * reports. + * Valid values for the second parameter: + * Pu = 0 <- or omitted -> default to character cells. + * Pu = 1 <- device physical pixels. + * Pu = 2 <- character cells. + */Terminal.prototype.enableLocatorReporting=function(params){var val=params[0]>0;//this.mouseEvents = val; +//this.decLocator = val; +};/** + * CSI Pt; Pl; Pb; Pr$ z + * Erase Rectangular Area (DECERA), VT400 and up. + * Pt; Pl; Pb; Pr denotes the rectangle. + * NOTE: xterm doesn't enable this code by default. + */Terminal.prototype.eraseRectangle=function(params){var t=params[0],l=params[1],b=params[2],r=params[3];var line,i,ch;ch=[this.eraseAttr(),' ',1];// xterm? +for(;t47);}function matchColor(r1,g1,b1){var hash=r1<<16|g1<<8|b1;if(matchColor._cache[hash]!=null){return matchColor._cache[hash];}var ldiff=Infinity,li=-1,i=0,c,r2,g2,b2,diff;for(;iCOMBINING[max][1])return false;while(max>=min){mid=Math.floor((min+max)/2);if(ucs>COMBINING[mid][1])min=mid+1;else if(ucs=0x7f&&ucs<0xa0)return opts.control;// binary search in table of non-spacing characters +if(bisearch(ucs))return 0;// if we arrive here, ucs is not a combining or C0/C1 control character +return 1+(ucs>=0x1100&&(ucs<=0x115f||// Hangul Jamo init. consonants +ucs==0x2329||ucs==0x232a||ucs>=0x2e80&&ucs<=0xa4cf&&ucs!=0x303f||// CJK..Yi +ucs>=0xac00&&ucs<=0xd7a3||// Hangul Syllables +ucs>=0xf900&&ucs<=0xfaff||// CJK Compat Ideographs +ucs>=0xfe10&&ucs<=0xfe19||// Vertical forms +ucs>=0xfe30&&ucs<=0xfe6f||// CJK Compat Forms +ucs>=0xff00&&ucs<=0xff60||// Fullwidth Forms +ucs>=0xffe0&&ucs<=0xffe6||ucs>=0x20000&&ucs<=0x2fffd||ucs>=0x30000&&ucs<=0x3fffd));}return wcwidth;}({nul:0,control:0});// configurable options +/** + * Expose + */Terminal.EventEmitter=_EventEmitter.EventEmitter;Terminal.CompositionHelper=_CompositionHelper.CompositionHelper;Terminal.Viewport=_Viewport.Viewport;Terminal.inherits=inherits;/** + * Adds an event listener to the terminal. + * + * @param {string} event The name of the event. TODO: Document all event types + * @param {function} callback The function to call when the event is triggered. + */Terminal.on=on;Terminal.off=off;Terminal.cancel=cancel;module.exports=Terminal; + +},{"./CompositionHelper.js":1,"./EventEmitter.js":2,"./Viewport.js":3,"./handlers/Clipboard.js":4,"./utils/Browser":5}]},{},[7])(7) +}); +//# sourceMappingURL=xterm.js.map diff --git a/vendor/assets/stylesheets/xterm/xterm.css b/vendor/assets/stylesheets/xterm/xterm.css new file mode 100644 index 00000000000..b30d7b493f1 --- /dev/null +++ b/vendor/assets/stylesheets/xterm/xterm.css @@ -0,0 +1,2206 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/* + * Default style for xterm.js + */ + +.terminal { + background-color: #000; + color: #fff; + font-family: courier-new, courier, monospace; + font-feature-settings: "liga" 0; + position: relative; +} + +.terminal.focus, +.terminal:focus { + outline: none; +} + +.terminal .xterm-helpers { + position: absolute; + top: 0; +} + +.terminal .xterm-helper-textarea { + /* + * HACK: to fix IE's blinking cursor + * Move textarea out of the screen to the far left, so that the cursor is not visible. + */ + position: absolute; + opacity: 0; + left: -9999em; + top: -9999em; + width: 0; + height: 0; + z-index: -10; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.terminal .terminal-cursor { + background-color: #fff; + color: #000; +} + +.terminal:not(.focus) .terminal-cursor { + outline: 1px solid #fff; + outline-offset: -1px; + background-color: transparent; +} + +.terminal.focus .terminal-cursor.blinking { + animation: blink-cursor 1.2s infinite step-end; +} + +@keyframes blink-cursor { + 0% { + background-color: #fff; + color: #000; + } + 50% { + background-color: transparent; + color: #FFF; + } +} + +.terminal .composition-view { + background: #000; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.terminal .composition-view.active { + display: block; +} + +.terminal .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: #000; + overflow-y: scroll; +} + +.terminal .xterm-rows { + position: absolute; + left: 0; + top: 0; +} + +.terminal .xterm-rows > div { + /* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */ + white-space: nowrap; +} + +.terminal .xterm-scroll-area { + visibility: hidden; +} + +.terminal .xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + left: -9999em; +} + +/* + * Determine default colors for xterm.js + */ +.terminal .xterm-bold { + font-weight: bold; +} + +.terminal .xterm-underline { + text-decoration: underline; +} + +.terminal .xterm-blink { + text-decoration: blink; +} + +.terminal .xterm-hidden { + visibility: hidden; +} + +.terminal .xterm-color-0 { + color: #2e3436; +} + +.terminal .xterm-bg-color-0 { + background-color: #2e3436; +} + +.terminal .xterm-color-1 { + color: #cc0000; +} + +.terminal .xterm-bg-color-1 { + background-color: #cc0000; +} + +.terminal .xterm-color-2 { + color: #4e9a06; +} + +.terminal .xterm-bg-color-2 { + background-color: #4e9a06; +} + +.terminal .xterm-color-3 { + color: #c4a000; +} + +.terminal .xterm-bg-color-3 { + background-color: #c4a000; +} + +.terminal .xterm-color-4 { + color: #3465a4; +} + +.terminal .xterm-bg-color-4 { + background-color: #3465a4; +} + +.terminal .xterm-color-5 { + color: #75507b; +} + +.terminal .xterm-bg-color-5 { + background-color: #75507b; +} + +.terminal .xterm-color-6 { + color: #06989a; +} + +.terminal .xterm-bg-color-6 { + background-color: #06989a; +} + +.terminal .xterm-color-7 { + color: #d3d7cf; +} + +.terminal .xterm-bg-color-7 { + background-color: #d3d7cf; +} + +.terminal .xterm-color-8 { + color: #555753; +} + +.terminal .xterm-bg-color-8 { + background-color: #555753; +} + +.terminal .xterm-color-9 { + color: #ef2929; +} + +.terminal .xterm-bg-color-9 { + background-color: #ef2929; +} + +.terminal .xterm-color-10 { + color: #8ae234; +} + +.terminal .xterm-bg-color-10 { + background-color: #8ae234; +} + +.terminal .xterm-color-11 { + color: #fce94f; +} + +.terminal .xterm-bg-color-11 { + background-color: #fce94f; +} + +.terminal .xterm-color-12 { + color: #729fcf; +} + +.terminal .xterm-bg-color-12 { + background-color: #729fcf; +} + +.terminal .xterm-color-13 { + color: #ad7fa8; +} + +.terminal .xterm-bg-color-13 { + background-color: #ad7fa8; +} + +.terminal .xterm-color-14 { + color: #34e2e2; +} + +.terminal .xterm-bg-color-14 { + background-color: #34e2e2; +} + +.terminal .xterm-color-15 { + color: #eeeeec; +} + +.terminal .xterm-bg-color-15 { + background-color: #eeeeec; +} + +.terminal .xterm-color-16 { + color: #000000; +} + +.terminal .xterm-bg-color-16 { + background-color: #000000; +} + +.terminal .xterm-color-17 { + color: #00005f; +} + +.terminal .xterm-bg-color-17 { + background-color: #00005f; +} + +.terminal .xterm-color-18 { + color: #000087; +} + +.terminal .xterm-bg-color-18 { + background-color: #000087; +} + +.terminal .xterm-color-19 { + color: #0000af; +} + +.terminal .xterm-bg-color-19 { + background-color: #0000af; +} + +.terminal .xterm-color-20 { + color: #0000d7; +} + +.terminal .xterm-bg-color-20 { + background-color: #0000d7; +} + +.terminal .xterm-color-21 { + color: #0000ff; +} + +.terminal .xterm-bg-color-21 { + background-color: #0000ff; +} + +.terminal .xterm-color-22 { + color: #005f00; +} + +.terminal .xterm-bg-color-22 { + background-color: #005f00; +} + +.terminal .xterm-color-23 { + color: #005f5f; +} + +.terminal .xterm-bg-color-23 { + background-color: #005f5f; +} + +.terminal .xterm-color-24 { + color: #005f87; +} + +.terminal .xterm-bg-color-24 { + background-color: #005f87; +} + +.terminal .xterm-color-25 { + color: #005faf; +} + +.terminal .xterm-bg-color-25 { + background-color: #005faf; +} + +.terminal .xterm-color-26 { + color: #005fd7; +} + +.terminal .xterm-bg-color-26 { + background-color: #005fd7; +} + +.terminal .xterm-color-27 { + color: #005fff; +} + +.terminal .xterm-bg-color-27 { + background-color: #005fff; +} + +.terminal .xterm-color-28 { + color: #008700; +} + +.terminal .xterm-bg-color-28 { + background-color: #008700; +} + +.terminal .xterm-color-29 { + color: #00875f; +} + +.terminal .xterm-bg-color-29 { + background-color: #00875f; +} + +.terminal .xterm-color-30 { + color: #008787; +} + +.terminal .xterm-bg-color-30 { + background-color: #008787; +} + +.terminal .xterm-color-31 { + color: #0087af; +} + +.terminal .xterm-bg-color-31 { + background-color: #0087af; +} + +.terminal .xterm-color-32 { + color: #0087d7; +} + +.terminal .xterm-bg-color-32 { + background-color: #0087d7; +} + +.terminal .xterm-color-33 { + color: #0087ff; +} + +.terminal .xterm-bg-color-33 { + background-color: #0087ff; +} + +.terminal .xterm-color-34 { + color: #00af00; +} + +.terminal .xterm-bg-color-34 { + background-color: #00af00; +} + +.terminal .xterm-color-35 { + color: #00af5f; +} + +.terminal .xterm-bg-color-35 { + background-color: #00af5f; +} + +.terminal .xterm-color-36 { + color: #00af87; +} + +.terminal .xterm-bg-color-36 { + background-color: #00af87; +} + +.terminal .xterm-color-37 { + color: #00afaf; +} + +.terminal .xterm-bg-color-37 { + background-color: #00afaf; +} + +.terminal .xterm-color-38 { + color: #00afd7; +} + +.terminal .xterm-bg-color-38 { + background-color: #00afd7; +} + +.terminal .xterm-color-39 { + color: #00afff; +} + +.terminal .xterm-bg-color-39 { + background-color: #00afff; +} + +.terminal .xterm-color-40 { + color: #00d700; +} + +.terminal .xterm-bg-color-40 { + background-color: #00d700; +} + +.terminal .xterm-color-41 { + color: #00d75f; +} + +.terminal .xterm-bg-color-41 { + background-color: #00d75f; +} + +.terminal .xterm-color-42 { + color: #00d787; +} + +.terminal .xterm-bg-color-42 { + background-color: #00d787; +} + +.terminal .xterm-color-43 { + color: #00d7af; +} + +.terminal .xterm-bg-color-43 { + background-color: #00d7af; +} + +.terminal .xterm-color-44 { + color: #00d7d7; +} + +.terminal .xterm-bg-color-44 { + background-color: #00d7d7; +} + +.terminal .xterm-color-45 { + color: #00d7ff; +} + +.terminal .xterm-bg-color-45 { + background-color: #00d7ff; +} + +.terminal .xterm-color-46 { + color: #00ff00; +} + +.terminal .xterm-bg-color-46 { + background-color: #00ff00; +} + +.terminal .xterm-color-47 { + color: #00ff5f; +} + +.terminal .xterm-bg-color-47 { + background-color: #00ff5f; +} + +.terminal .xterm-color-48 { + color: #00ff87; +} + +.terminal .xterm-bg-color-48 { + background-color: #00ff87; +} + +.terminal .xterm-color-49 { + color: #00ffaf; +} + +.terminal .xterm-bg-color-49 { + background-color: #00ffaf; +} + +.terminal .xterm-color-50 { + color: #00ffd7; +} + +.terminal .xterm-bg-color-50 { + background-color: #00ffd7; +} + +.terminal .xterm-color-51 { + color: #00ffff; +} + +.terminal .xterm-bg-color-51 { + background-color: #00ffff; +} + +.terminal .xterm-color-52 { + color: #5f0000; +} + +.terminal .xterm-bg-color-52 { + background-color: #5f0000; +} + +.terminal .xterm-color-53 { + color: #5f005f; +} + +.terminal .xterm-bg-color-53 { + background-color: #5f005f; +} + +.terminal .xterm-color-54 { + color: #5f0087; +} + +.terminal .xterm-bg-color-54 { + background-color: #5f0087; +} + +.terminal .xterm-color-55 { + color: #5f00af; +} + +.terminal .xterm-bg-color-55 { + background-color: #5f00af; +} + +.terminal .xterm-color-56 { + color: #5f00d7; +} + +.terminal .xterm-bg-color-56 { + background-color: #5f00d7; +} + +.terminal .xterm-color-57 { + color: #5f00ff; +} + +.terminal .xterm-bg-color-57 { + background-color: #5f00ff; +} + +.terminal .xterm-color-58 { + color: #5f5f00; +} + +.terminal .xterm-bg-color-58 { + background-color: #5f5f00; +} + +.terminal .xterm-color-59 { + color: #5f5f5f; +} + +.terminal .xterm-bg-color-59 { + background-color: #5f5f5f; +} + +.terminal .xterm-color-60 { + color: #5f5f87; +} + +.terminal .xterm-bg-color-60 { + background-color: #5f5f87; +} + +.terminal .xterm-color-61 { + color: #5f5faf; +} + +.terminal .xterm-bg-color-61 { + background-color: #5f5faf; +} + +.terminal .xterm-color-62 { + color: #5f5fd7; +} + +.terminal .xterm-bg-color-62 { + background-color: #5f5fd7; +} + +.terminal .xterm-color-63 { + color: #5f5fff; +} + +.terminal .xterm-bg-color-63 { + background-color: #5f5fff; +} + +.terminal .xterm-color-64 { + color: #5f8700; +} + +.terminal .xterm-bg-color-64 { + background-color: #5f8700; +} + +.terminal .xterm-color-65 { + color: #5f875f; +} + +.terminal .xterm-bg-color-65 { + background-color: #5f875f; +} + +.terminal .xterm-color-66 { + color: #5f8787; +} + +.terminal .xterm-bg-color-66 { + background-color: #5f8787; +} + +.terminal .xterm-color-67 { + color: #5f87af; +} + +.terminal .xterm-bg-color-67 { + background-color: #5f87af; +} + +.terminal .xterm-color-68 { + color: #5f87d7; +} + +.terminal .xterm-bg-color-68 { + background-color: #5f87d7; +} + +.terminal .xterm-color-69 { + color: #5f87ff; +} + +.terminal .xterm-bg-color-69 { + background-color: #5f87ff; +} + +.terminal .xterm-color-70 { + color: #5faf00; +} + +.terminal .xterm-bg-color-70 { + background-color: #5faf00; +} + +.terminal .xterm-color-71 { + color: #5faf5f; +} + +.terminal .xterm-bg-color-71 { + background-color: #5faf5f; +} + +.terminal .xterm-color-72 { + color: #5faf87; +} + +.terminal .xterm-bg-color-72 { + background-color: #5faf87; +} + +.terminal .xterm-color-73 { + color: #5fafaf; +} + +.terminal .xterm-bg-color-73 { + background-color: #5fafaf; +} + +.terminal .xterm-color-74 { + color: #5fafd7; +} + +.terminal .xterm-bg-color-74 { + background-color: #5fafd7; +} + +.terminal .xterm-color-75 { + color: #5fafff; +} + +.terminal .xterm-bg-color-75 { + background-color: #5fafff; +} + +.terminal .xterm-color-76 { + color: #5fd700; +} + +.terminal .xterm-bg-color-76 { + background-color: #5fd700; +} + +.terminal .xterm-color-77 { + color: #5fd75f; +} + +.terminal .xterm-bg-color-77 { + background-color: #5fd75f; +} + +.terminal .xterm-color-78 { + color: #5fd787; +} + +.terminal .xterm-bg-color-78 { + background-color: #5fd787; +} + +.terminal .xterm-color-79 { + color: #5fd7af; +} + +.terminal .xterm-bg-color-79 { + background-color: #5fd7af; +} + +.terminal .xterm-color-80 { + color: #5fd7d7; +} + +.terminal .xterm-bg-color-80 { + background-color: #5fd7d7; +} + +.terminal .xterm-color-81 { + color: #5fd7ff; +} + +.terminal .xterm-bg-color-81 { + background-color: #5fd7ff; +} + +.terminal .xterm-color-82 { + color: #5fff00; +} + +.terminal .xterm-bg-color-82 { + background-color: #5fff00; +} + +.terminal .xterm-color-83 { + color: #5fff5f; +} + +.terminal .xterm-bg-color-83 { + background-color: #5fff5f; +} + +.terminal .xterm-color-84 { + color: #5fff87; +} + +.terminal .xterm-bg-color-84 { + background-color: #5fff87; +} + +.terminal .xterm-color-85 { + color: #5fffaf; +} + +.terminal .xterm-bg-color-85 { + background-color: #5fffaf; +} + +.terminal .xterm-color-86 { + color: #5fffd7; +} + +.terminal .xterm-bg-color-86 { + background-color: #5fffd7; +} + +.terminal .xterm-color-87 { + color: #5fffff; +} + +.terminal .xterm-bg-color-87 { + background-color: #5fffff; +} + +.terminal .xterm-color-88 { + color: #870000; +} + +.terminal .xterm-bg-color-88 { + background-color: #870000; +} + +.terminal .xterm-color-89 { + color: #87005f; +} + +.terminal .xterm-bg-color-89 { + background-color: #87005f; +} + +.terminal .xterm-color-90 { + color: #870087; +} + +.terminal .xterm-bg-color-90 { + background-color: #870087; +} + +.terminal .xterm-color-91 { + color: #8700af; +} + +.terminal .xterm-bg-color-91 { + background-color: #8700af; +} + +.terminal .xterm-color-92 { + color: #8700d7; +} + +.terminal .xterm-bg-color-92 { + background-color: #8700d7; +} + +.terminal .xterm-color-93 { + color: #8700ff; +} + +.terminal .xterm-bg-color-93 { + background-color: #8700ff; +} + +.terminal .xterm-color-94 { + color: #875f00; +} + +.terminal .xterm-bg-color-94 { + background-color: #875f00; +} + +.terminal .xterm-color-95 { + color: #875f5f; +} + +.terminal .xterm-bg-color-95 { + background-color: #875f5f; +} + +.terminal .xterm-color-96 { + color: #875f87; +} + +.terminal .xterm-bg-color-96 { + background-color: #875f87; +} + +.terminal .xterm-color-97 { + color: #875faf; +} + +.terminal .xterm-bg-color-97 { + background-color: #875faf; +} + +.terminal .xterm-color-98 { + color: #875fd7; +} + +.terminal .xterm-bg-color-98 { + background-color: #875fd7; +} + +.terminal .xterm-color-99 { + color: #875fff; +} + +.terminal .xterm-bg-color-99 { + background-color: #875fff; +} + +.terminal .xterm-color-100 { + color: #878700; +} + +.terminal .xterm-bg-color-100 { + background-color: #878700; +} + +.terminal .xterm-color-101 { + color: #87875f; +} + +.terminal .xterm-bg-color-101 { + background-color: #87875f; +} + +.terminal .xterm-color-102 { + color: #878787; +} + +.terminal .xterm-bg-color-102 { + background-color: #878787; +} + +.terminal .xterm-color-103 { + color: #8787af; +} + +.terminal .xterm-bg-color-103 { + background-color: #8787af; +} + +.terminal .xterm-color-104 { + color: #8787d7; +} + +.terminal .xterm-bg-color-104 { + background-color: #8787d7; +} + +.terminal .xterm-color-105 { + color: #8787ff; +} + +.terminal .xterm-bg-color-105 { + background-color: #8787ff; +} + +.terminal .xterm-color-106 { + color: #87af00; +} + +.terminal .xterm-bg-color-106 { + background-color: #87af00; +} + +.terminal .xterm-color-107 { + color: #87af5f; +} + +.terminal .xterm-bg-color-107 { + background-color: #87af5f; +} + +.terminal .xterm-color-108 { + color: #87af87; +} + +.terminal .xterm-bg-color-108 { + background-color: #87af87; +} + +.terminal .xterm-color-109 { + color: #87afaf; +} + +.terminal .xterm-bg-color-109 { + background-color: #87afaf; +} + +.terminal .xterm-color-110 { + color: #87afd7; +} + +.terminal .xterm-bg-color-110 { + background-color: #87afd7; +} + +.terminal .xterm-color-111 { + color: #87afff; +} + +.terminal .xterm-bg-color-111 { + background-color: #87afff; +} + +.terminal .xterm-color-112 { + color: #87d700; +} + +.terminal .xterm-bg-color-112 { + background-color: #87d700; +} + +.terminal .xterm-color-113 { + color: #87d75f; +} + +.terminal .xterm-bg-color-113 { + background-color: #87d75f; +} + +.terminal .xterm-color-114 { + color: #87d787; +} + +.terminal .xterm-bg-color-114 { + background-color: #87d787; +} + +.terminal .xterm-color-115 { + color: #87d7af; +} + +.terminal .xterm-bg-color-115 { + background-color: #87d7af; +} + +.terminal .xterm-color-116 { + color: #87d7d7; +} + +.terminal .xterm-bg-color-116 { + background-color: #87d7d7; +} + +.terminal .xterm-color-117 { + color: #87d7ff; +} + +.terminal .xterm-bg-color-117 { + background-color: #87d7ff; +} + +.terminal .xterm-color-118 { + color: #87ff00; +} + +.terminal .xterm-bg-color-118 { + background-color: #87ff00; +} + +.terminal .xterm-color-119 { + color: #87ff5f; +} + +.terminal .xterm-bg-color-119 { + background-color: #87ff5f; +} + +.terminal .xterm-color-120 { + color: #87ff87; +} + +.terminal .xterm-bg-color-120 { + background-color: #87ff87; +} + +.terminal .xterm-color-121 { + color: #87ffaf; +} + +.terminal .xterm-bg-color-121 { + background-color: #87ffaf; +} + +.terminal .xterm-color-122 { + color: #87ffd7; +} + +.terminal .xterm-bg-color-122 { + background-color: #87ffd7; +} + +.terminal .xterm-color-123 { + color: #87ffff; +} + +.terminal .xterm-bg-color-123 { + background-color: #87ffff; +} + +.terminal .xterm-color-124 { + color: #af0000; +} + +.terminal .xterm-bg-color-124 { + background-color: #af0000; +} + +.terminal .xterm-color-125 { + color: #af005f; +} + +.terminal .xterm-bg-color-125 { + background-color: #af005f; +} + +.terminal .xterm-color-126 { + color: #af0087; +} + +.terminal .xterm-bg-color-126 { + background-color: #af0087; +} + +.terminal .xterm-color-127 { + color: #af00af; +} + +.terminal .xterm-bg-color-127 { + background-color: #af00af; +} + +.terminal .xterm-color-128 { + color: #af00d7; +} + +.terminal .xterm-bg-color-128 { + background-color: #af00d7; +} + +.terminal .xterm-color-129 { + color: #af00ff; +} + +.terminal .xterm-bg-color-129 { + background-color: #af00ff; +} + +.terminal .xterm-color-130 { + color: #af5f00; +} + +.terminal .xterm-bg-color-130 { + background-color: #af5f00; +} + +.terminal .xterm-color-131 { + color: #af5f5f; +} + +.terminal .xterm-bg-color-131 { + background-color: #af5f5f; +} + +.terminal .xterm-color-132 { + color: #af5f87; +} + +.terminal .xterm-bg-color-132 { + background-color: #af5f87; +} + +.terminal .xterm-color-133 { + color: #af5faf; +} + +.terminal .xterm-bg-color-133 { + background-color: #af5faf; +} + +.terminal .xterm-color-134 { + color: #af5fd7; +} + +.terminal .xterm-bg-color-134 { + background-color: #af5fd7; +} + +.terminal .xterm-color-135 { + color: #af5fff; +} + +.terminal .xterm-bg-color-135 { + background-color: #af5fff; +} + +.terminal .xterm-color-136 { + color: #af8700; +} + +.terminal .xterm-bg-color-136 { + background-color: #af8700; +} + +.terminal .xterm-color-137 { + color: #af875f; +} + +.terminal .xterm-bg-color-137 { + background-color: #af875f; +} + +.terminal .xterm-color-138 { + color: #af8787; +} + +.terminal .xterm-bg-color-138 { + background-color: #af8787; +} + +.terminal .xterm-color-139 { + color: #af87af; +} + +.terminal .xterm-bg-color-139 { + background-color: #af87af; +} + +.terminal .xterm-color-140 { + color: #af87d7; +} + +.terminal .xterm-bg-color-140 { + background-color: #af87d7; +} + +.terminal .xterm-color-141 { + color: #af87ff; +} + +.terminal .xterm-bg-color-141 { + background-color: #af87ff; +} + +.terminal .xterm-color-142 { + color: #afaf00; +} + +.terminal .xterm-bg-color-142 { + background-color: #afaf00; +} + +.terminal .xterm-color-143 { + color: #afaf5f; +} + +.terminal .xterm-bg-color-143 { + background-color: #afaf5f; +} + +.terminal .xterm-color-144 { + color: #afaf87; +} + +.terminal .xterm-bg-color-144 { + background-color: #afaf87; +} + +.terminal .xterm-color-145 { + color: #afafaf; +} + +.terminal .xterm-bg-color-145 { + background-color: #afafaf; +} + +.terminal .xterm-color-146 { + color: #afafd7; +} + +.terminal .xterm-bg-color-146 { + background-color: #afafd7; +} + +.terminal .xterm-color-147 { + color: #afafff; +} + +.terminal .xterm-bg-color-147 { + background-color: #afafff; +} + +.terminal .xterm-color-148 { + color: #afd700; +} + +.terminal .xterm-bg-color-148 { + background-color: #afd700; +} + +.terminal .xterm-color-149 { + color: #afd75f; +} + +.terminal .xterm-bg-color-149 { + background-color: #afd75f; +} + +.terminal .xterm-color-150 { + color: #afd787; +} + +.terminal .xterm-bg-color-150 { + background-color: #afd787; +} + +.terminal .xterm-color-151 { + color: #afd7af; +} + +.terminal .xterm-bg-color-151 { + background-color: #afd7af; +} + +.terminal .xterm-color-152 { + color: #afd7d7; +} + +.terminal .xterm-bg-color-152 { + background-color: #afd7d7; +} + +.terminal .xterm-color-153 { + color: #afd7ff; +} + +.terminal .xterm-bg-color-153 { + background-color: #afd7ff; +} + +.terminal .xterm-color-154 { + color: #afff00; +} + +.terminal .xterm-bg-color-154 { + background-color: #afff00; +} + +.terminal .xterm-color-155 { + color: #afff5f; +} + +.terminal .xterm-bg-color-155 { + background-color: #afff5f; +} + +.terminal .xterm-color-156 { + color: #afff87; +} + +.terminal .xterm-bg-color-156 { + background-color: #afff87; +} + +.terminal .xterm-color-157 { + color: #afffaf; +} + +.terminal .xterm-bg-color-157 { + background-color: #afffaf; +} + +.terminal .xterm-color-158 { + color: #afffd7; +} + +.terminal .xterm-bg-color-158 { + background-color: #afffd7; +} + +.terminal .xterm-color-159 { + color: #afffff; +} + +.terminal .xterm-bg-color-159 { + background-color: #afffff; +} + +.terminal .xterm-color-160 { + color: #d70000; +} + +.terminal .xterm-bg-color-160 { + background-color: #d70000; +} + +.terminal .xterm-color-161 { + color: #d7005f; +} + +.terminal .xterm-bg-color-161 { + background-color: #d7005f; +} + +.terminal .xterm-color-162 { + color: #d70087; +} + +.terminal .xterm-bg-color-162 { + background-color: #d70087; +} + +.terminal .xterm-color-163 { + color: #d700af; +} + +.terminal .xterm-bg-color-163 { + background-color: #d700af; +} + +.terminal .xterm-color-164 { + color: #d700d7; +} + +.terminal .xterm-bg-color-164 { + background-color: #d700d7; +} + +.terminal .xterm-color-165 { + color: #d700ff; +} + +.terminal .xterm-bg-color-165 { + background-color: #d700ff; +} + +.terminal .xterm-color-166 { + color: #d75f00; +} + +.terminal .xterm-bg-color-166 { + background-color: #d75f00; +} + +.terminal .xterm-color-167 { + color: #d75f5f; +} + +.terminal .xterm-bg-color-167 { + background-color: #d75f5f; +} + +.terminal .xterm-color-168 { + color: #d75f87; +} + +.terminal .xterm-bg-color-168 { + background-color: #d75f87; +} + +.terminal .xterm-color-169 { + color: #d75faf; +} + +.terminal .xterm-bg-color-169 { + background-color: #d75faf; +} + +.terminal .xterm-color-170 { + color: #d75fd7; +} + +.terminal .xterm-bg-color-170 { + background-color: #d75fd7; +} + +.terminal .xterm-color-171 { + color: #d75fff; +} + +.terminal .xterm-bg-color-171 { + background-color: #d75fff; +} + +.terminal .xterm-color-172 { + color: #d78700; +} + +.terminal .xterm-bg-color-172 { + background-color: #d78700; +} + +.terminal .xterm-color-173 { + color: #d7875f; +} + +.terminal .xterm-bg-color-173 { + background-color: #d7875f; +} + +.terminal .xterm-color-174 { + color: #d78787; +} + +.terminal .xterm-bg-color-174 { + background-color: #d78787; +} + +.terminal .xterm-color-175 { + color: #d787af; +} + +.terminal .xterm-bg-color-175 { + background-color: #d787af; +} + +.terminal .xterm-color-176 { + color: #d787d7; +} + +.terminal .xterm-bg-color-176 { + background-color: #d787d7; +} + +.terminal .xterm-color-177 { + color: #d787ff; +} + +.terminal .xterm-bg-color-177 { + background-color: #d787ff; +} + +.terminal .xterm-color-178 { + color: #d7af00; +} + +.terminal .xterm-bg-color-178 { + background-color: #d7af00; +} + +.terminal .xterm-color-179 { + color: #d7af5f; +} + +.terminal .xterm-bg-color-179 { + background-color: #d7af5f; +} + +.terminal .xterm-color-180 { + color: #d7af87; +} + +.terminal .xterm-bg-color-180 { + background-color: #d7af87; +} + +.terminal .xterm-color-181 { + color: #d7afaf; +} + +.terminal .xterm-bg-color-181 { + background-color: #d7afaf; +} + +.terminal .xterm-color-182 { + color: #d7afd7; +} + +.terminal .xterm-bg-color-182 { + background-color: #d7afd7; +} + +.terminal .xterm-color-183 { + color: #d7afff; +} + +.terminal .xterm-bg-color-183 { + background-color: #d7afff; +} + +.terminal .xterm-color-184 { + color: #d7d700; +} + +.terminal .xterm-bg-color-184 { + background-color: #d7d700; +} + +.terminal .xterm-color-185 { + color: #d7d75f; +} + +.terminal .xterm-bg-color-185 { + background-color: #d7d75f; +} + +.terminal .xterm-color-186 { + color: #d7d787; +} + +.terminal .xterm-bg-color-186 { + background-color: #d7d787; +} + +.terminal .xterm-color-187 { + color: #d7d7af; +} + +.terminal .xterm-bg-color-187 { + background-color: #d7d7af; +} + +.terminal .xterm-color-188 { + color: #d7d7d7; +} + +.terminal .xterm-bg-color-188 { + background-color: #d7d7d7; +} + +.terminal .xterm-color-189 { + color: #d7d7ff; +} + +.terminal .xterm-bg-color-189 { + background-color: #d7d7ff; +} + +.terminal .xterm-color-190 { + color: #d7ff00; +} + +.terminal .xterm-bg-color-190 { + background-color: #d7ff00; +} + +.terminal .xterm-color-191 { + color: #d7ff5f; +} + +.terminal .xterm-bg-color-191 { + background-color: #d7ff5f; +} + +.terminal .xterm-color-192 { + color: #d7ff87; +} + +.terminal .xterm-bg-color-192 { + background-color: #d7ff87; +} + +.terminal .xterm-color-193 { + color: #d7ffaf; +} + +.terminal .xterm-bg-color-193 { + background-color: #d7ffaf; +} + +.terminal .xterm-color-194 { + color: #d7ffd7; +} + +.terminal .xterm-bg-color-194 { + background-color: #d7ffd7; +} + +.terminal .xterm-color-195 { + color: #d7ffff; +} + +.terminal .xterm-bg-color-195 { + background-color: #d7ffff; +} + +.terminal .xterm-color-196 { + color: #ff0000; +} + +.terminal .xterm-bg-color-196 { + background-color: #ff0000; +} + +.terminal .xterm-color-197 { + color: #ff005f; +} + +.terminal .xterm-bg-color-197 { + background-color: #ff005f; +} + +.terminal .xterm-color-198 { + color: #ff0087; +} + +.terminal .xterm-bg-color-198 { + background-color: #ff0087; +} + +.terminal .xterm-color-199 { + color: #ff00af; +} + +.terminal .xterm-bg-color-199 { + background-color: #ff00af; +} + +.terminal .xterm-color-200 { + color: #ff00d7; +} + +.terminal .xterm-bg-color-200 { + background-color: #ff00d7; +} + +.terminal .xterm-color-201 { + color: #ff00ff; +} + +.terminal .xterm-bg-color-201 { + background-color: #ff00ff; +} + +.terminal .xterm-color-202 { + color: #ff5f00; +} + +.terminal .xterm-bg-color-202 { + background-color: #ff5f00; +} + +.terminal .xterm-color-203 { + color: #ff5f5f; +} + +.terminal .xterm-bg-color-203 { + background-color: #ff5f5f; +} + +.terminal .xterm-color-204 { + color: #ff5f87; +} + +.terminal .xterm-bg-color-204 { + background-color: #ff5f87; +} + +.terminal .xterm-color-205 { + color: #ff5faf; +} + +.terminal .xterm-bg-color-205 { + background-color: #ff5faf; +} + +.terminal .xterm-color-206 { + color: #ff5fd7; +} + +.terminal .xterm-bg-color-206 { + background-color: #ff5fd7; +} + +.terminal .xterm-color-207 { + color: #ff5fff; +} + +.terminal .xterm-bg-color-207 { + background-color: #ff5fff; +} + +.terminal .xterm-color-208 { + color: #ff8700; +} + +.terminal .xterm-bg-color-208 { + background-color: #ff8700; +} + +.terminal .xterm-color-209 { + color: #ff875f; +} + +.terminal .xterm-bg-color-209 { + background-color: #ff875f; +} + +.terminal .xterm-color-210 { + color: #ff8787; +} + +.terminal .xterm-bg-color-210 { + background-color: #ff8787; +} + +.terminal .xterm-color-211 { + color: #ff87af; +} + +.terminal .xterm-bg-color-211 { + background-color: #ff87af; +} + +.terminal .xterm-color-212 { + color: #ff87d7; +} + +.terminal .xterm-bg-color-212 { + background-color: #ff87d7; +} + +.terminal .xterm-color-213 { + color: #ff87ff; +} + +.terminal .xterm-bg-color-213 { + background-color: #ff87ff; +} + +.terminal .xterm-color-214 { + color: #ffaf00; +} + +.terminal .xterm-bg-color-214 { + background-color: #ffaf00; +} + +.terminal .xterm-color-215 { + color: #ffaf5f; +} + +.terminal .xterm-bg-color-215 { + background-color: #ffaf5f; +} + +.terminal .xterm-color-216 { + color: #ffaf87; +} + +.terminal .xterm-bg-color-216 { + background-color: #ffaf87; +} + +.terminal .xterm-color-217 { + color: #ffafaf; +} + +.terminal .xterm-bg-color-217 { + background-color: #ffafaf; +} + +.terminal .xterm-color-218 { + color: #ffafd7; +} + +.terminal .xterm-bg-color-218 { + background-color: #ffafd7; +} + +.terminal .xterm-color-219 { + color: #ffafff; +} + +.terminal .xterm-bg-color-219 { + background-color: #ffafff; +} + +.terminal .xterm-color-220 { + color: #ffd700; +} + +.terminal .xterm-bg-color-220 { + background-color: #ffd700; +} + +.terminal .xterm-color-221 { + color: #ffd75f; +} + +.terminal .xterm-bg-color-221 { + background-color: #ffd75f; +} + +.terminal .xterm-color-222 { + color: #ffd787; +} + +.terminal .xterm-bg-color-222 { + background-color: #ffd787; +} + +.terminal .xterm-color-223 { + color: #ffd7af; +} + +.terminal .xterm-bg-color-223 { + background-color: #ffd7af; +} + +.terminal .xterm-color-224 { + color: #ffd7d7; +} + +.terminal .xterm-bg-color-224 { + background-color: #ffd7d7; +} + +.terminal .xterm-color-225 { + color: #ffd7ff; +} + +.terminal .xterm-bg-color-225 { + background-color: #ffd7ff; +} + +.terminal .xterm-color-226 { + color: #ffff00; +} + +.terminal .xterm-bg-color-226 { + background-color: #ffff00; +} + +.terminal .xterm-color-227 { + color: #ffff5f; +} + +.terminal .xterm-bg-color-227 { + background-color: #ffff5f; +} + +.terminal .xterm-color-228 { + color: #ffff87; +} + +.terminal .xterm-bg-color-228 { + background-color: #ffff87; +} + +.terminal .xterm-color-229 { + color: #ffffaf; +} + +.terminal .xterm-bg-color-229 { + background-color: #ffffaf; +} + +.terminal .xterm-color-230 { + color: #ffffd7; +} + +.terminal .xterm-bg-color-230 { + background-color: #ffffd7; +} + +.terminal .xterm-color-231 { + color: #ffffff; +} + +.terminal .xterm-bg-color-231 { + background-color: #ffffff; +} + +.terminal .xterm-color-232 { + color: #080808; +} + +.terminal .xterm-bg-color-232 { + background-color: #080808; +} + +.terminal .xterm-color-233 { + color: #121212; +} + +.terminal .xterm-bg-color-233 { + background-color: #121212; +} + +.terminal .xterm-color-234 { + color: #1c1c1c; +} + +.terminal .xterm-bg-color-234 { + background-color: #1c1c1c; +} + +.terminal .xterm-color-235 { + color: #262626; +} + +.terminal .xterm-bg-color-235 { + background-color: #262626; +} + +.terminal .xterm-color-236 { + color: #303030; +} + +.terminal .xterm-bg-color-236 { + background-color: #303030; +} + +.terminal .xterm-color-237 { + color: #3a3a3a; +} + +.terminal .xterm-bg-color-237 { + background-color: #3a3a3a; +} + +.terminal .xterm-color-238 { + color: #444444; +} + +.terminal .xterm-bg-color-238 { + background-color: #444444; +} + +.terminal .xterm-color-239 { + color: #4e4e4e; +} + +.terminal .xterm-bg-color-239 { + background-color: #4e4e4e; +} + +.terminal .xterm-color-240 { + color: #585858; +} + +.terminal .xterm-bg-color-240 { + background-color: #585858; +} + +.terminal .xterm-color-241 { + color: #626262; +} + +.terminal .xterm-bg-color-241 { + background-color: #626262; +} + +.terminal .xterm-color-242 { + color: #6c6c6c; +} + +.terminal .xterm-bg-color-242 { + background-color: #6c6c6c; +} + +.terminal .xterm-color-243 { + color: #767676; +} + +.terminal .xterm-bg-color-243 { + background-color: #767676; +} + +.terminal .xterm-color-244 { + color: #808080; +} + +.terminal .xterm-bg-color-244 { + background-color: #808080; +} + +.terminal .xterm-color-245 { + color: #8a8a8a; +} + +.terminal .xterm-bg-color-245 { + background-color: #8a8a8a; +} + +.terminal .xterm-color-246 { + color: #949494; +} + +.terminal .xterm-bg-color-246 { + background-color: #949494; +} + +.terminal .xterm-color-247 { + color: #9e9e9e; +} + +.terminal .xterm-bg-color-247 { + background-color: #9e9e9e; +} + +.terminal .xterm-color-248 { + color: #a8a8a8; +} + +.terminal .xterm-bg-color-248 { + background-color: #a8a8a8; +} + +.terminal .xterm-color-249 { + color: #b2b2b2; +} + +.terminal .xterm-bg-color-249 { + background-color: #b2b2b2; +} + +.terminal .xterm-color-250 { + color: #bcbcbc; +} + +.terminal .xterm-bg-color-250 { + background-color: #bcbcbc; +} + +.terminal .xterm-color-251 { + color: #c6c6c6; +} + +.terminal .xterm-bg-color-251 { + background-color: #c6c6c6; +} + +.terminal .xterm-color-252 { + color: #d0d0d0; +} + +.terminal .xterm-bg-color-252 { + background-color: #d0d0d0; +} + +.terminal .xterm-color-253 { + color: #dadada; +} + +.terminal .xterm-bg-color-253 { + background-color: #dadada; +} + +.terminal .xterm-color-254 { + color: #e4e4e4; +} + +.terminal .xterm-bg-color-254 { + background-color: #e4e4e4; +} + +.terminal .xterm-color-255 { + color: #eeeeee; +} + +.terminal .xterm-bg-color-255 { + background-color: #eeeeee; +} -- cgit v1.2.1 From 5378302763e1a461bab5213aa379d5b9e6dc322c Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Sat, 17 Dec 2016 04:09:50 +0000 Subject: Add a ReactiveCaching concern for use in the KubernetesService --- app/models/concerns/reactive_caching.rb | 110 +++++++++++++++++++ app/workers/reactive_caching_worker.rb | 15 +++ config/sidekiq_queues.yml | 1 + spec/models/concerns/reactive_caching_spec.rb | 145 ++++++++++++++++++++++++++ spec/support/reactive_caching_helpers.rb | 38 +++++++ spec/workers/reactive_caching_worker_spec.rb | 15 +++ 6 files changed, 324 insertions(+) create mode 100644 app/models/concerns/reactive_caching.rb create mode 100644 app/workers/reactive_caching_worker.rb create mode 100644 spec/models/concerns/reactive_caching_spec.rb create mode 100644 spec/support/reactive_caching_helpers.rb create mode 100644 spec/workers/reactive_caching_worker_spec.rb diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb new file mode 100644 index 00000000000..2db67a3b57f --- /dev/null +++ b/app/models/concerns/reactive_caching.rb @@ -0,0 +1,110 @@ +# The ReactiveCaching concern is used to fetch some data in the background and +# store it in the Rails cache, keeping it up-to-date for as long as it is being +# requested. If the data hasn't been requested for +reactive_cache_lifetime+, +# it stop being refreshed, and then be removed. +# +# Example of use: +# +# class Foo < ActiveRecord::Base +# include ReactiveCaching +# +# self.reactive_cache_key = ->(thing) { ["foo", thing.id] } +# +# after_save :clear_reactive_cache! +# +# def calculate_reactive_cache +# # Expensive operation here. The return value of this method is cached +# end +# +# def result +# with_reactive_cache do |data| +# # ... +# end +# end +# end +# +# In this example, the first time `#result` is called, it will return `nil`. +# However, it will enqueue a background worker to call `#calculate_reactive_cache` +# and set an initial cache lifetime of ten minutes. +# +# Each time the background job completes, it stores the return value of +# `#calculate_reactive_cache`. It is also re-enqueued to run again after +# `reactive_cache_refresh_interval`, so keeping the stored value up to date. +# Calculations are never run concurrently. +# +# Calling `#result` while a value is in the cache will call the block given to +# `#with_reactive_cache`, yielding the cached value. It will also extend the +# lifetime by `reactive_cache_lifetime`. +# +# Once the lifetime has expired, no more background jobs will be enqueued and +# calling `#result` will again return `nil` - starting the process all over +# again +module ReactiveCaching + extend ActiveSupport::Concern + + included do + class_attribute :reactive_cache_lease_timeout + + class_attribute :reactive_cache_key + class_attribute :reactive_cache_lifetime + class_attribute :reactive_cache_refresh_interval + + # defaults + self.reactive_cache_lease_timeout = 2.minutes + + self.reactive_cache_refresh_interval = 1.minute + self.reactive_cache_lifetime = 10.minutes + + def with_reactive_cache(&blk) + within_reactive_cache_lifetime do + data = Rails.cache.read(full_reactive_cache_key) + yield data if data.present? + end + ensure + Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id) + end + + def clear_reactive_cache! + Rails.cache.delete(full_reactive_cache_key) + end + + def exclusively_update_reactive_cache! + locking_reactive_cache do + within_reactive_cache_lifetime do + enqueuing_update do + value = calculate_reactive_cache + Rails.cache.write(full_reactive_cache_key, value) + end + end + end + end + + private + + def full_reactive_cache_key(*qualifiers) + prefix = self.class.reactive_cache_key + prefix = prefix.call(self) if prefix.respond_to?(:call) + + ([prefix].flatten + qualifiers).join(':') + end + + def locking_reactive_cache + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout) + uuid = lease.try_obtain + yield if uuid + ensure + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid) + end + + def within_reactive_cache_lifetime + yield if Rails.cache.read(full_reactive_cache_key('alive')) + end + + def enqueuing_update + yield + ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id) + end + end +end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb new file mode 100644 index 00000000000..9af9dae04f0 --- /dev/null +++ b/app/workers/reactive_caching_worker.rb @@ -0,0 +1,15 @@ +class ReactiveCachingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(class_name, id) + klass = begin + Kernel.const_get(class_name) + rescue NameError + nil + end + return unless klass + + klass.find_by(id: id).try(:exclusively_update_reactive_cache!) + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 69136b73946..c22964179d9 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -46,5 +46,6 @@ - [repository_check, 1] - [system_hook, 1] - [git_garbage_collect, 1] + - [reactive_caching, 1] - [cronjob, 1] - [default, 1] diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb new file mode 100644 index 00000000000..a0765a264cf --- /dev/null +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe ReactiveCaching, caching: true do + include ReactiveCachingHelpers + + class CacheTest + include ReactiveCaching + + self.reactive_cache_key = ->(thing) { ["foo", thing.id] } + + self.reactive_cache_lifetime = 5.minutes + self.reactive_cache_refresh_interval = 15.seconds + + attr_reader :id + + def initialize(id, &blk) + @id = id + @calculator = blk + end + + def calculate_reactive_cache + @calculator.call + end + + def result + with_reactive_cache do |data| + data / 2 + end + end + end + + let(:now) { Time.now.utc } + + around(:each) do |example| + Timecop.freeze(now) { example.run } + end + + let(:calculation) { -> { 2 + 2 } } + let(:cache_key) { "foo:666" } + let(:instance) { CacheTest.new(666, &calculation) } + + describe '#with_reactive_cache' do + before { stub_reactive_cache } + subject(:go!) { instance.result } + + context 'when cache is empty' do + it { is_expected.to be_nil } + + it 'queues a background worker' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) + + go! + end + + it 'updates the cache lifespan' do + go! + + expect(reactive_cache_alive?(instance)).to be_truthy + end + end + + context 'when the cache is full' do + before { stub_reactive_cache(instance, 4) } + + it { is_expected.to eq(2) } + + context 'and expired' do + before { invalidate_reactive_cache(instance) } + it { is_expected.to be_nil } + end + end + end + + describe '#clear_reactive_cache!' do + before do + stub_reactive_cache(instance, 4) + instance.clear_reactive_cache! + end + + it { expect(instance.result).to be_nil } + end + + describe '#exclusively_update_reactive_cache!' do + subject(:go!) { instance.exclusively_update_reactive_cache! } + + context 'when the lease is free and lifetime is not exceeded' do + before { stub_reactive_cache(instance, "preexisting") } + + it 'takes and releases the lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") + expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000") + + go! + end + + it 'caches the result of #calculate_reactive_cache' do + go! + + expect(read_reactive_cache(instance)).to eq(calculation.call) + end + + it "enqueues a repeat worker" do + expect_reactive_cache_update_queued(instance) + + go! + end + + context 'and #calculate_reactive_cache raises an exception' do + before { stub_reactive_cache(instance, "preexisting") } + let(:calculation) { -> { raise "foo"} } + + it 'leaves the cache untouched' do + expect { go! }.to raise_error("foo") + expect(read_reactive_cache(instance)).to eq("preexisting") + end + + it 'enqueues a repeat worker' do + expect_reactive_cache_update_queued(instance) + + expect { go! }.to raise_error("foo") + end + end + end + + context 'when lifetime is exceeded' do + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + + context 'when the lease is already taken' do + before do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil) + end + + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + end +end diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb new file mode 100644 index 00000000000..279db3c5748 --- /dev/null +++ b/spec/support/reactive_caching_helpers.rb @@ -0,0 +1,38 @@ +module ReactiveCachingHelpers + def reactive_cache_key(subject, *qualifiers) + ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':') + end + + def stub_reactive_cache(subject = nil, data = nil) + allow(ReactiveCachingWorker).to receive(:perform_async) + allow(ReactiveCachingWorker).to receive(:perform_in) + write_reactive_cache(subject, data) if data + end + + def read_reactive_cache(subject) + Rails.cache.read(reactive_cache_key(subject)) + end + + def write_reactive_cache(subject, data) + start_reactive_cache_lifetime(subject) + Rails.cache.write(reactive_cache_key(subject), data) + end + + def reactive_cache_alive?(subject) + Rails.cache.read(reactive_cache_key(subject, 'alive')) + end + + def invalidate_reactive_cache(subject) + Rails.cache.delete(reactive_cache_key(subject, 'alive')) + end + + def start_reactive_cache_lifetime(subject) + Rails.cache.write(reactive_cache_key(subject, 'alive'), true) + end + + def expect_reactive_cache_update_queued(subject) + expect(ReactiveCachingWorker). + to receive(:perform_in). + with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id) + end +end diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb new file mode 100644 index 00000000000..5f4453c15d6 --- /dev/null +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe ReactiveCachingWorker do + let(:project) { create(:kubernetes_project) } + let(:service) { project.deployment_service } + subject { described_class.new.perform("KubernetesService", service.id) } + + describe '#perform' do + it 'calls #exclusively_update_reactive_cache!' do + expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!) + + subject + end + end +end -- cgit v1.2.1 From c3d972f4e861059312c2708dacb57999416fcc70 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 22 Nov 2016 19:55:56 +0000 Subject: Add terminals to the Kubernetes deployment service --- .../projects/environments_controller.rb | 25 ++++- app/models/concerns/reactive_caching.rb | 4 + app/models/environment.rb | 8 ++ app/models/project_services/deployment_service.rb | 18 ++++ app/models/project_services/kubernetes_service.rb | 67 ++++++++++--- app/serializers/environment_entity.rb | 8 ++ lib/gitlab/kubernetes.rb | 80 +++++++++++++++ lib/gitlab/workhorse.rb | 13 +++ .../projects/environments_controller_spec.rb | 68 +++++++++++++ spec/factories/projects.rb | 2 +- spec/lib/gitlab/kubernetes_spec.rb | 39 ++++++++ spec/lib/gitlab/workhorse_spec.rb | 36 +++++++ spec/models/environment_spec.rb | 60 +++++++++++- .../project_services/kubernetes_service_spec.rb | 109 ++++++++++++++++----- spec/support/kubernetes_helpers.rb | 52 ++++++++++ 15 files changed, 546 insertions(+), 43 deletions(-) create mode 100644 lib/gitlab/kubernetes.rb create mode 100644 spec/lib/gitlab/kubernetes_spec.rb create mode 100644 spec/support/kubernetes_helpers.rb diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 6bd4cb3f2f5..a1b39c6a78a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -9,7 +9,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def index @scope = params[:scope] @environments = project.environments - + respond_to do |format| format.html format.json do @@ -56,6 +56,29 @@ class Projects::EnvironmentsController < Projects::ApplicationController redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) end + def terminal + # Currently, this acts as a hint to load the terminal details into the cache + # if they aren't there already. In the future, users will need these details + # to choose between terminals to connect to. + @terminals = environment.terminals + end + + # GET .../terminal.ws : implemented in gitlab-workhorse + def terminal_websocket_authorize + Gitlab::Workhorse.verify_api_request!(request.headers) + + # Just return the first terminal for now. If the list is in the process of + # being looked up, this may result in a 404 response, so the frontend + # should retry + terminal = environment.terminals.try(:first) + if terminal + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.terminal_websocket(terminal) + else + render text: 'Not found', status: 404 + end + end + private def environment_params diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 2db67a3b57f..944519a3070 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -55,6 +55,10 @@ module ReactiveCaching self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 10.minutes + def calculate_reactive_cache + raise NotImplementedError + end + def with_reactive_cache(&blk) within_reactive_cache_lifetime do data = Rails.cache.read(full_reactive_cache_key) diff --git a/app/models/environment.rb b/app/models/environment.rb index 8ef1c841ea3..5cde94b3509 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base end end + def has_terminals? + project.deployment_service.present? && available? && last_deployment.present? + end + + def terminals + project.deployment_service.terminals(self) if has_terminals? + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index da6be9dd7b7..ab353a1abe6 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -12,4 +12,22 @@ class DeploymentService < Service def predefined_variables [] end + + # Environments may have a number of terminals. Should return an array of + # hashes describing them, e.g.: + # + # [{ + # :selectors => {"a" => "b", "foo" => "bar"}, + # :url => "wss://external.example.com/exec", + # :headers => {"Authorization" => "Token xxx"}, + # :subprotocols => ["foo"], + # :ca_pem => "----BEGIN CERTIFICATE...", # optional + # :created_at => Time.now.utc + # }] + # + # Selectors should be a set of values that uniquely identify a particular + # terminal + def terminals(environment) + raise NotImplementedError + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f5fbf8b353b..085125ca9dc 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,4 +1,9 @@ class KubernetesService < DeploymentService + include Gitlab::Kubernetes + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + # Namespace defaults to the project path, but can be overridden in case that # is an invalid or inappropriate name prop_accessor :namespace @@ -25,6 +30,8 @@ class KubernetesService < DeploymentService length: 1..63 end + after_save :clear_reactive_cache! + def initialize_properties if properties.nil? self.properties = {} @@ -41,7 +48,8 @@ class KubernetesService < DeploymentService end def help - '' + 'To enable terminal access to Kubernetes environments, label your ' \ + 'deployments with `app=$CI_ENVIRONMENT_SLUG`' end def to_param @@ -75,9 +83,9 @@ class KubernetesService < DeploymentService # Check we can connect to the Kubernetes API def test(*args) - kubeclient = build_kubeclient - kubeclient.discover + kubeclient = build_kubeclient! + kubeclient.discover { success: kubeclient.discovered, result: "Checked API discovery endpoint" } rescue => err { success: false, result: err } @@ -93,20 +101,48 @@ class KubernetesService < DeploymentService variables end - private + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = data.fetch(:pods, nil) + filter_pods(pods, app: environment.slug). + flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. + map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + end + end - def build_kubeclient(api_path = '/api', api_version = 'v1') - return nil unless api_url && namespace && token + # Caches all pods in the namespace so other calls don't need to block on + # network access. + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? - url = URI.parse(api_url) - url.path = url.path[0..-2] if url.path[-1] == "/" - url.path += api_path + kubeclient = build_kubeclient! + + # Store as hashes, rather than as third-party types + pods = begin + kubeclient.get_pods(namespace: namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # We may want to cache extra things in the future + { pods: pods } + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && namespace && token ::Kubeclient::Client.new( - url, + join_api_url(api_path), api_version, - ssl_options: kubeclient_ssl_options, auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] ) end @@ -125,4 +161,13 @@ class KubernetesService < DeploymentService def kubeclient_auth_options { bearer_token: token } end + + def join_api_url(*parts) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [ prefix, *parts ].join("/") + + url.to_s + end end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 7e0fc9c071e..e7ef01258ef 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -8,6 +8,7 @@ class EnvironmentEntity < Grape::Entity expose :environment_type expose :last_deployment, using: DeploymentEntity expose :stoppable? + expose :has_terminals?, as: :has_terminals expose :environment_path do |environment| namespace_project_environment_path( @@ -23,5 +24,12 @@ class EnvironmentEntity < Grape::Entity environment) end + expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| + terminal_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :created_at, :updated_at end diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb new file mode 100644 index 00000000000..288771c1c12 --- /dev/null +++ b/lib/gitlab/kubernetes.rb @@ -0,0 +1,80 @@ +module Gitlab + # Helper methods to do with Kubernetes network services & resources + module Kubernetes + # This is the comand that is run to start a terminal session. Kubernetes + # expects `command=foo&command=bar, not `command[]=foo&command[]=bar` + EXEC_COMMAND = URI.encode_www_form( + ['sh', '-c', 'bash || sh'].map { |value| ['command', value] } + ) + + # Filters an array of pods (as returned by the kubernetes API) by their labels + def filter_pods(pods, labels = {}) + pods.select do |pod| + metadata = pod.fetch("metadata", {}) + pod_labels = metadata.fetch("labels", nil) + next unless pod_labels + + labels.all? { |k, v| pod_labels[k.to_s] == v } + end + end + + # Converts a pod (as returned by the kubernetes API) into a terminal + def terminals_for_pod(api_url, namespace, pod) + metadata = pod.fetch("metadata", {}) + status = pod.fetch("status", {}) + spec = pod.fetch("spec", {}) + + containers = spec["containers"] + pod_name = metadata["name"] + phase = status["phase"] + + return unless containers.present? && pod_name.present? && phase == "Running" + + created_at = DateTime.parse(metadata["creationTimestamp"]) rescue nil + + containers.map do |container| + { + selectors: { pod: pod_name, container: container["name"] }, + url: container_exec_url(api_url, namespace, pod_name, container["name"]), + subprotocols: ['channel.k8s.io'], + headers: Hash.new { |h, k| h[k] = [] }, + created_at: created_at, + } + end + end + + def add_terminal_auth(terminal, token, ca_pem = nil) + terminal[:headers]['Authorization'] << "Bearer #{token}" + terminal[:ca_pem] = ca_pem if ca_pem.present? + terminal + end + + def container_exec_url(api_url, namespace, pod_name, container_name) + url = URI.parse(api_url) + url.path = [ + url.path.sub(%r{/+\z}, ''), + 'api', 'v1', + 'namespaces', ERB::Util.url_encode(namespace), + 'pods', ERB::Util.url_encode(pod_name), + 'exec' + ].join('/') + + url.query = { + container: container_name, + tty: true, + stdin: true, + stdout: true, + stderr: true, + }.to_query + '&' + EXEC_COMMAND + + case url.scheme + when 'http' + url.scheme = 'ws' + when 'https' + url.scheme = 'wss' + end + + url.to_s + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index aeb1a26e1ba..d28bb583fe7 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -95,6 +95,19 @@ module Gitlab ] end + def terminal_websocket(terminal) + details = { + 'Terminal' => { + 'Subprotocols' => terminal[:subprotocols], + 'Url' => terminal[:url], + 'Header' => terminal[:headers] + } + } + details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem) + + details + end + def version path = Rails.root.join(VERSION_FILE) path.readable? ? path.read.chomp : 'unknown' diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index bc5e2711125..7afa8b1bc28 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -71,6 +71,74 @@ describe Projects::EnvironmentsController do end end + describe 'GET #terminal' do + context 'with valid id' do + it 'responds with a status code 200' do + get :terminal, environment_params + + expect(response).to have_http_status(200) + end + + it 'loads the terminals for the enviroment' do + expect_any_instance_of(Environment).to receive(:terminals) + + get :terminal, environment_params + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + get :terminal, environment_params(id: 666) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET #terminal_websocket_authorize' do + context 'with valid workhorse signature' do + before do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil) + end + + context 'and valid id' do + it 'returns the first terminal for the environment' do + expect_any_instance_of(Environment). + to receive(:terminals). + and_return([:fake_terminal]) + + expect(Gitlab::Workhorse). + to receive(:terminal_websocket). + with(:fake_terminal). + and_return(workhorse: :response) + + get :terminal_websocket_authorize, environment_params + + expect(response).to have_http_status(200) + expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(response.body).to eq('{"workhorse":"response"}') + end + end + + context 'and invalid id' do + it 'returns 404' do + get :terminal_websocket_authorize, environment_params(id: 666) + expect(response).to have_http_status(404) + end + end + end + + context 'with invalid workhorse signature' do + it 'aborts with an exception' do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError) + + expect { get :terminal_websocket_authorize, environment_params }.to raise_error(JWT::DecodeError) + # controller tests don't set the response status correctly. It's enough + # to check that the action raised an exception + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 0d072d6a690..c941fb5ef4b 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -140,7 +140,7 @@ FactoryGirl.define do active: true, properties: { namespace: project.path, - api_url: 'https://kubernetes.example.com/api', + api_url: 'https://kubernetes.example.com', token: 'a' * 40, } ) diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb new file mode 100644 index 00000000000..c9bd52a3b8f --- /dev/null +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes do + include described_class + + describe '#container_exec_url' do + let(:api_url) { 'https://example.com' } + let(:namespace) { 'default' } + let(:pod_name) { 'pod1' } + let(:container_name) { 'container1' } + + subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) } + + it { expect(result.scheme).to eq('wss') } + it { expect(result.host).to eq('example.com') } + it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') } + it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') } + + context 'with a HTTP API URL' do + let(:api_url) { 'http://example.com' } + + it { expect(result.scheme).to eq('ws') } + end + + context 'with a path prefix in the API URL' do + let(:api_url) { 'https://example.com/prefix/' } + it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') } + end + + context 'with arguments that need urlencoding' do + let(:namespace) { 'default namespace' } + let(:pod_name) { 'pod 1' } + let(:container_name) { 'container 1' } + + it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') } + it { expect(result.query).to match(/\Acontainer=container\+1&/) } + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b5b685da904..61da91dcbd3 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do end end + describe '.terminal_websocket' do + def terminal(ca_pem: nil) + out = { + subprotocols: ['foo'], + url: 'wss://example.com/terminal.ws', + headers: { 'Authorization' => ['Token x'] } + } + out[:ca_pem] = ca_pem if ca_pem + out + end + + def workhorse(ca_pem: nil) + out = { + 'Terminal' => { + 'Subprotocols' => ['foo'], + 'Url' => 'wss://example.com/terminal.ws', + 'Header' => { 'Authorization' => ['Token x'] } + } + } + out['Terminal']['CAPem'] = ca_pem if ca_pem + out + end + + context 'without ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal) } + + it { is_expected.to eq(workhorse) } + end + + context 'with ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) } + + it { is_expected.to eq(workhorse(ca_pem: "foo")) } + end + end + describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 97cbb093ed2..93eb402e060 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Environment, models: true do - subject(:environment) { create(:environment) } + let(:project) { create(:empty_project) } + subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -31,6 +32,8 @@ describe Environment, models: true do end describe '#includes_commit?' do + let(:project) { create(:project) } + context 'without a last deployment' do it "returns false" do expect(environment.includes_commit?('HEAD')).to be false @@ -38,9 +41,6 @@ describe Environment, models: true do end context 'with a last deployment' do - let(:project) { create(:project) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do create(:deployment, environment: environment, sha: project.commit('master').id) end @@ -65,7 +65,6 @@ describe Environment, models: true do describe '#first_deployment_for' do let(:project) { create(:project) } - let!(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } let(:head_commit) { project.commit } @@ -196,6 +195,57 @@ describe Environment, models: true do end end + describe '#has_terminals?' do + subject { environment.has_terminals? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:kubernetes_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a deployment service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:kubernetes_project) } + before { environment.stop } + it { is_expected.to be_falsy } + end + end + + describe '#terminals' do + let(:project) { create(:kubernetes_project) } + subject { environment.terminals } + + context 'when the environment has terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(true) } + + it 'returns the terminals from the deployment service' do + expect(project.deployment_service). + to receive(:terminals).with(environment). + and_return(:fake_terminals) + + is_expected.to eq(:fake_terminals) + end + end + + context 'when the environment does not have terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(false) } + it { is_expected.to eq(nil) } + end + end + describe '#slug' do it "is automatically generated" do expect(environment.slug).not_to be_nil diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 3603602e41d..4f3cd14e941 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -1,7 +1,29 @@ require 'spec_helper' -describe KubernetesService, models: true do - let(:project) { create(:empty_project) } +describe KubernetesService, models: true, caching: true do + include KubernetesHelpers + include ReactiveCachingHelpers + + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + + # We use Kubeclient to interactive with the Kubernetes API. It will + # GET /api/v1 for a list of resources the API supports. This must be stubbed + # in addition to any other HTTP requests we expect it to perform. + let(:discovery_url) { service.api_url + '/api/v1' } + let(:discovery_response) { { body: kube_discovery_body.to_json } } + + let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" } + let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } + + def stub_kubeclient_discover + WebMock.stub_request(:get, discovery_url).to_return(discovery_response) + end + + def stub_kubeclient_pods + stub_kubeclient_discover + WebMock.stub_request(:get, pods_url).to_return(pods_response) + end describe "Associations" do it { is_expected.to belong_to :project } @@ -65,22 +87,15 @@ describe KubernetesService, models: true do end describe '#test' do - let(:project) { create(:kubernetes_project) } - let(:service) { project.kubernetes_service } - let(:discovery_url) { service.api_url + '/api/v1' } - - # JSON response body from Kubernetes GET /api/v1 request - let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json } + before do + stub_kubeclient_discover + end context 'with path prefix in api_url' do let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } - before do - service.api_url = 'https://kubernetes.example.com/prefix/' - end - it 'tests with the prefix' do - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + service.api_url = 'https://kubernetes.example.com/prefix/' expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -88,17 +103,12 @@ describe KubernetesService, models: true do end context 'with custom CA certificate' do - let(:certificate) { "CA PEM DATA" } - before do - service.update_attributes!(ca_pem: certificate) - end - it 'is added to the certificate store' do - cert = double("certificate") + service.ca_pem = "CA PEM DATA" - expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) + cert = double("certificate") + expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert) expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -107,17 +117,15 @@ describe KubernetesService, models: true do context 'success' do it 'reads the discovery endpoint' do - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) - expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once end end context 'failure' do - it 'fails to read the discovery endpoint' do - WebMock.stub_request(:get, discovery_url).to_return(status: 404) + let(:discovery_response) { { status: 404 } } + it 'fails to read the discovery endpoint' do expect(service.test[:success]).to be_falsy expect(WebMock).to have_requested(:get, discovery_url).once end @@ -156,4 +164,55 @@ describe KubernetesService, models: true do ) end end + + describe '#terminals' do + let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + subject { service.terminals(environment) } + + context 'with invalid pods' do + it 'returns no terminals' do + stub_reactive_cache(service, pods: [ { "bad" => "pod" } ]) + + is_expected.to be_empty + end + end + + context 'with valid pods' do + let(:pod) { kube_pod(app: environment.slug) } + let(:terminals) { kube_terminals(service, pod) } + + it 'returns terminals' do + stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) + + is_expected.to eq(terminals + terminals) + end + end + end + + describe '#calculate_reactive_cache' do + before { stub_kubeclient_pods } + subject { service.calculate_reactive_cache } + + context 'when service is inactive' do + before { service.active = false } + + it { is_expected.to be_nil } + end + + context 'when kubernetes responds with valid pods' do + it { is_expected.to eq(pods: [kube_pod]) } + end + + context 'when kubernetes responds with 500' do + let(:pods_response) { { status: 500 } } + + it { expect { subject }.to raise_error(KubeException) } + end + + context 'when kubernetes responds with 404' do + let(:pods_response) { { status: 404 } } + + it { is_expected.to eq(pods: []) } + end + end end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb new file mode 100644 index 00000000000..6c4c246a68b --- /dev/null +++ b/spec/support/kubernetes_helpers.rb @@ -0,0 +1,52 @@ +module KubernetesHelpers + include Gitlab::Kubernetes + + def kube_discovery_body + { "kind" => "APIResourceList", + "resources" => [ + { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + ], + } + end + + def kube_pods_body(*pods) + { "kind" => "PodList", + "items" => [ kube_pod ], + } + end + + # This is a partial response, it will have many more elements in reality but + # these are the ones we care about at the moment + def kube_pod(app: "valid-pod-label") + { "metadata" => { + "name" => "kube-pod", + "creationTimestamp" => "2016-11-25T19:55:19Z", + "labels" => { "app" => app }, + }, + "spec" => { + "containers" => [ + { "name" => "container-0" }, + { "name" => "container-1" }, + ], + }, + "status" => { "phase" => "Running" }, + } + end + + def kube_terminals(service, pod) + pod_name = pod['metadata']['name'] + containers = pod['spec']['containers'] + + containers.map do |container| + terminal = { + selectors: { pod: pod_name, container: container['name'] }, + url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), + subprotocols: ['channel.k8s.io'], + headers: { 'Authorization' => ["Bearer #{service.token}"] }, + created_at: DateTime.parse(pod['metadata']['creationTimestamp']) + } + terminal[:ca_pem] = service.ca_pem if service.ca_pem.present? + terminal + end + end +end -- cgit v1.2.1 From f347d5fcadb4b05f367ed571b27e73040f625520 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 19 Dec 2016 14:13:06 -0600 Subject: Remove inline-block styling from status --- app/assets/stylesheets/framework/page-header.scss | 1 - app/assets/stylesheets/pages/status.scss | 1 - 2 files changed, 2 deletions(-) diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index fff7d7f7524..625bea96aaa 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -57,7 +57,6 @@ } .ci-status-link { - svg { position: relative; top: 2px; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 055dacd81f4..a810ed32327 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,6 +1,5 @@ .container-fluid { .ci-status { - display: inline-block; padding: 2px 7px; margin-right: 10px; border: 1px solid $gray-darker; -- cgit v1.2.1 From 95e0fac59ae8174d11873e95a3ef579af476f215 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 19 Dec 2016 22:36:47 +0200 Subject: Fix Route#rename_children behavior Given group `gitlab` and `gitlab-org` exists. When rename `gitlab` it will rename `gitlab-org` group route too. This commit fixes it Signed-off-by: Dmitriy Zaporozhets --- app/models/route.rb | 2 +- changelogs/unreleased/dz-fix-route-rename.yml | 4 ++++ spec/models/route_spec.rb | 8 +++++--- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/dz-fix-route-rename.yml diff --git a/app/models/route.rb b/app/models/route.rb index d40214b9da6..caf596efa79 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -13,7 +13,7 @@ class Route < ActiveRecord::Base def rename_children # We update each row separately because MySQL does not have regexp_replace. # rubocop:disable Rails/FindEach - Route.where('path LIKE ?', "#{path_was}%").each do |route| + Route.where('path LIKE ?', "#{path_was}/%").each do |route| # Note that update column skips validation and callbacks. # We need this to avoid recursive call of rename_children method route.update_column(:path, route.path.sub(path_was, path)) diff --git a/changelogs/unreleased/dz-fix-route-rename.yml b/changelogs/unreleased/dz-fix-route-rename.yml new file mode 100644 index 00000000000..a649fb169a5 --- /dev/null +++ b/changelogs/unreleased/dz-fix-route-rename.yml @@ -0,0 +1,4 @@ +--- +title: Fix Route#rename_children behavior +merge_request: +author: diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 6f491fdf9a0..8481a9bef16 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Route, models: true do - let!(:group) { create(:group) } + let!(:group) { create(:group, path: 'gitlab') } let!(:route) { group.route } describe 'relationships' do @@ -17,13 +17,15 @@ describe Route, models: true do describe '#rename_children' do let!(:nested_group) { create(:group, path: "test", parent: group) } let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } + let!(:similar_group) { create(:group, path: 'gitlab-org') } - it "updates children routes with new path" do - route.update_attributes(path: 'bar') + before { route.update_attributes(path: 'bar') } + it "updates children routes with new path" do expect(described_class.exists?(path: 'bar')).to be_truthy expect(described_class.exists?(path: 'bar/test')).to be_truthy expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy + expect(described_class.exists?(path: 'gitlab-org')).to be_truthy end end end -- cgit v1.2.1 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: Minor adjustments API Mattermost [ci skip] --- app/controllers/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 | 35 ++++++++++++---------- 11 files changed, 91 insertions(+), 76 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) + + described_class.create(session, 'abc', url: 'http://trigger.com') + end + end - token = described_class.create('abc', url: 'http://trigger.url/trigger', icon_url: 'http://myicon.com/icon.png') + 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 - end + 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 service' do - allow_any_instance_of(Mattermost::Session).to receive(:with_session). - and_return('mynewtoken') + it 'saves the token' do + subject - expect { subject }.to change { project.services.count }.by(1) + expect(service.reload.token).to eq('mynewtoken') + end end - it 'saves the token' do - allow_any_instance_of(Mattermost::Session).to receive(:with_session). - and_return('mynewtoken') - - subject + 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(service.reload.token).to eq('mynewtoken') + expect(subject).to eq("Error") + end end end end -- cgit v1.2.1 From 00f076d71b3ba831051d627271e8b0cec6f5324b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 19 Dec 2016 14:54:07 -0600 Subject: Update font size of detail page header to 14px --- app/assets/stylesheets/pages/detail_page.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 80baebd5ea3..9b28df1afc5 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -2,7 +2,6 @@ padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; color: $gl-text-color-dark; - font-size: 16px; line-height: 34px; .author { -- cgit v1.2.1 From d41c8f48143acf865183a324e3c18dd286d423ba Mon Sep 17 00:00:00 2001 From: Ryan Harris Date: Fri, 16 Dec 2016 20:03:57 -0500 Subject: Even out padding on plus button in breadcrumb menu Changed plus button padding to 6px 10px --- app/assets/stylesheets/pages/tree.scss | 1 + changelogs/unreleased/25740-fix-new-branch-button-padding.yml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelogs/unreleased/25740-fix-new-branch-button-padding.yml diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index c0341db7289..13be08104e6 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -14,6 +14,7 @@ .add-to-tree { vertical-align: top; + padding: 6px 10px; } .tree-table { diff --git a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml new file mode 100644 index 00000000000..7da8f9357a7 --- /dev/null +++ b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml @@ -0,0 +1,4 @@ +--- +title: Made the padding on the plus button in the breadcrumb menu even +merge_request: +author: Ryan Harris -- cgit v1.2.1 From e06f88effa842c73d3827593f8d28846207bfca0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 22:12:30 +0100 Subject: Fix specs --- features/project/service.feature | 6 ++-- features/steps/project/services.rb | 6 ++-- .../projects/services/slack_slash_command_spec.rb | 36 +++++++++------------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/features/project/service.feature b/features/project/service.feature index 892db48d785..cce5f58adec 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -37,10 +37,10 @@ Feature: Project Services And I fill Assembla settings Then I should see Assembla service settings saved - Scenario: Activate Slack service + Scenario: Activate Slack notifications service When I visit project "Shop" services page - And I click Slack Notifications service link - And I fill Slack Notifications settings + And I click Slack notifications service link + And I fill Slack notifications settings Then I should see Slack Notifications service settings saved Scenario: Activate Pushover service diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 06a1afedbd9..a4d29770922 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -137,11 +137,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Colorize messages').value).to eq '1' end - step 'I click Slack Notifications service link' do - click_link 'Slack Notifications' + step 'I click Slack notifications service link' do + click_link 'Slack notifications' end - step 'I fill Slack Notifications settings' do + step 'I fill Slack notifications settings' do check 'Active' fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' click_button 'Save' diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index 70e203efcf5..32b32f7ae8e 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -12,37 +12,29 @@ feature 'Slack slash commands', feature: true do login_as(user) end - scenario 'user visits the slack slash command config page', js: true do - it 'shows a help message' do - visit edit_namespace_project_service_path(project.namespace, project, service) + scenario 'user visits the slack slash command config page and shows a help message', js: true do + visit edit_namespace_project_service_path(project.namespace, project, service) - wait_for_ajax + wait_for_ajax - expect(page).to have_content('This service allows GitLab users to perform common') - end + expect(page).to have_content('This service allows GitLab users to perform common') end - scenario 'saving a token' do - given(:token) { ('a'..'z').to_a.join } + scenario 'shows the token after saving' do + visit edit_namespace_project_service_path(project.namespace, project, service) - it 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + fill_in 'service_token', with: 'token' + click_on 'Save' - fill_in 'service_token', with: token - click_on 'Save' + value = find_field('service_token').value - value = find_field('service_token').value - - expect(value).to eq(token) - end + expect(value).to eq('token') end - scenario 'the trigger url' do - it 'shows the correct url' do - visit edit_namespace_project_service_path(project.namespace, project, service) + scenario 'shows the correct trigger url' do + visit edit_namespace_project_service_path(project.namespace, project, service) - value = find_field('url').value - expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") - end + value = find_field('url').value + expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") end end -- cgit v1.2.1 From 3db5b7033b13c21b904a21f751bc0f19156ea155 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 15 Dec 2016 00:59:04 +0000 Subject: Add terminal UI and controller actions --- .../environments/components/environment.js.es6 | 3 ++ .../components/environment_item.js.es6 | 16 +++++++++++ .../components/environment_terminal_button.js.es6 | 27 ++++++++++++++++++ app/assets/stylesheets/framework/buttons.scss | 7 +++++ app/assets/stylesheets/pages/pipelines.scss | 20 ++++++++++++++ .../projects/environments_controller.rb | 14 ++++++---- app/serializers/environment_entity.rb | 10 +++---- app/serializers/request_aware_entity.rb | 4 +++ .../environments/_terminal_button.html.haml | 3 ++ app/views/projects/environments/index.html.haml | 1 + app/views/projects/environments/show.html.haml | 1 + app/views/projects/environments/terminal.html.haml | 22 +++++++++++++++ app/views/shared/icons/_icon_terminal.svg | 1 + config/routes/project.rb | 2 ++ .../projects/environments_controller_spec.rb | 1 + spec/factories/projects.rb | 10 +++++-- spec/features/environment_spec.rb | 32 ++++++++++++++++++++++ spec/features/environments_spec.rb | 28 +++++++++++++++++++ 18 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment_terminal_button.js.es6 create mode 100644 app/views/projects/environments/_terminal_button.html.haml create mode 100644 app/views/projects/environments/terminal.html.haml create mode 100644 app/views/shared/icons/_icon_terminal.svg diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index d04adecd207..facd653fd72 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -76,6 +76,7 @@ helpPagePath: environmentsData.helpPagePath, commitIconSvg: environmentsData.commitIconSvg, playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, }; }, @@ -230,6 +231,7 @@ :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"> diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 4674d5202e6..b26a40aa268 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -8,6 +8,7 @@ /*= require ./environment_external_url */ /*= require ./environment_stop */ /*= require ./environment_rollback */ +/*= require ./environment_terminal_button */ (() => { /** @@ -33,6 +34,7 @@ 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, 'stop-component': window.gl.environmentsList.StopComponent, 'rollback-component': window.gl.environmentsList.RollbackComponent, + 'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent, }, props: { @@ -68,6 +70,12 @@ type: String, required: false, }, + + terminalIconSvg: { + type: String, + required: false, + }, + }, data() { @@ -506,6 +514,14 @@ +
+ + +
+
{ + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 59ff17ad2c1..a11f1cd7735 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -230,6 +230,13 @@ } } +.btn-terminal { + svg { + height: 14px; + width: 18px; + } +} + .btn-lg { padding: 12px 20px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index be22e7bdc79..7905d2f2e7c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -734,3 +734,23 @@ padding: 5px 5px 5px 7px; } } + +.terminal-icon { + margin-left: 3px; +} + +.terminal-container { + .content-block { + border-bottom: none; + } + + #terminal { + margin-top: 10px; + min-height: 450px; + box-sizing: border-box; + + > div { + min-height: 450px; + } + } +} diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index a1b39c6a78a..87cc36253f1 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -4,7 +4,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] - before_action :environment, only: [:show, :edit, :update, :stop] + before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :verify_api_request!, only: :terminal_websocket_authorize def index @scope = params[:scope] @@ -14,7 +16,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController format.html format.json do render json: EnvironmentSerializer - .new(project: @project) + .new(project: @project, user: current_user) .represent(@environments) end end @@ -65,11 +67,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController # GET .../terminal.ws : implemented in gitlab-workhorse def terminal_websocket_authorize - Gitlab::Workhorse.verify_api_request!(request.headers) - # Just return the first terminal for now. If the list is in the process of # being looked up, this may result in a 404 response, so the frontend - # should retry + # should retry those errors terminal = environment.terminals.try(:first) if terminal set_workhorse_internal_api_content_type @@ -81,6 +81,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController private + def verify_api_request! + Gitlab::Workhorse.verify_api_request!(request.headers) + end + def environment_params params.require(:environment).permit(:name, :external_url) end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index e7ef01258ef..5d15eb8d3d3 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -8,7 +8,6 @@ class EnvironmentEntity < Grape::Entity expose :environment_type expose :last_deployment, using: DeploymentEntity expose :stoppable? - expose :has_terminals?, as: :has_terminals expose :environment_path do |environment| namespace_project_environment_path( @@ -25,10 +24,11 @@ class EnvironmentEntity < Grape::Entity end expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| - terminal_namespace_project_environment_path( - environment.project.namespace, - environment.project, - environment) + can?(request.user, :admin_environment, environment.project) && + terminal_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) end expose :created_at, :updated_at diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index ff8c1142abc..e159d750cb7 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -8,4 +8,8 @@ module RequestAwareEntity def request @options.fetch(:request) end + + def can?(object, action, subject) + Ability.allowed?(object, action, subject) + end end diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml new file mode 100644 index 00000000000..97de9c95de7 --- /dev/null +++ b/app/views/projects/environments/_terminal_button.html.haml @@ -0,0 +1,3 @@ +- if environment.has_terminals? && can?(current_user, :admin_environment, @project) + = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do + = icon('terminal') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 6aae035b3e0..0c6f696f5b9 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -15,4 +15,5 @@ "help-page-path" => help_page_path("ci/environments"), "css-class" => container_class, "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), "play-icon-svg" => custom_icon("icon_play")}} diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index bcac73d3698..6e0d9456900 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml new file mode 100644 index 00000000000..a6726e509e0 --- /dev/null +++ b/app/views/projects/environments/terminal.html.haml @@ -0,0 +1,22 @@ +- @no_container = true +- page_title "Terminal for environment", @environment.name += render "projects/pipelines/head" + +- content_for :page_specific_javascripts do + = stylesheet_link_tag "xterm/xterm" + = page_specific_javascript_tag("terminal/terminal_bundle.js") + +%div{class: container_class} + .top-area + .row + .col-sm-6 + %h3.page-title + Terminal for environment + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + +.terminal-container{class: container_class} + #terminal{data:{project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws"}} diff --git a/app/views/shared/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg new file mode 100644 index 00000000000..c80f44c3edf --- /dev/null +++ b/app/views/shared/icons/_icon_terminal.svg @@ -0,0 +1 @@ + diff --git a/config/routes/project.rb b/config/routes/project.rb index e17d6bae10c..80cc47ab9a8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -148,6 +148,8 @@ constraints(ProjectUrlConstrainer.new) do resources :environments, except: [:destroy] do member do post :stop + get :terminal + get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7afa8b1bc28..7ac1d62d1b1 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -123,6 +123,7 @@ describe Projects::EnvironmentsController do context 'and invalid id' do it 'returns 404' do get :terminal_websocket_authorize, environment_params(id: 666) + expect(response).to have_http_status(404) end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c941fb5ef4b..f7fa834d7a2 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -42,6 +42,12 @@ FactoryGirl.define do end end + trait :test_repo do + after :create do |project| + TestEnv.copy_repo(project) + end + end + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -91,9 +97,7 @@ FactoryGirl.define do factory :project, parent: :empty_project do path { 'gitlabhq' } - after :create do |project| - TestEnv.copy_repo(project) - end + test_repo end factory :forked_project_with_submodules, parent: :empty_project do diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 0c1939fd885..56f6cd2e095 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -38,6 +38,10 @@ feature 'Environment', :feature do scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') end + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end end context 'with related deployable present' do @@ -60,6 +64,10 @@ feature 'Environment', :feature do expect(page).not_to have_link('Stop') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } @@ -84,6 +92,26 @@ feature 'Environment', :feature do end end + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end + context 'with stop action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } @@ -158,4 +186,8 @@ feature 'Environment', :feature do environment.project, environment) end + + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index e1b97b31e5d..72b984cfab8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do expect(page).not_to have_css('external-url') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } @@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do end end end + + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end end end end @@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do end end + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end + def visit_environments(project) visit namespace_project_environments_path(project.namespace, project) end -- cgit v1.2.1 From 7b7781654e9c01f5928850df0daaf50fd1d89e8a Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 16 Dec 2016 21:41:33 +0000 Subject: Add changelog entry --- .../nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml 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 new file mode 100644 index 00000000000..bb4edf80d94 --- /dev/null +++ b/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml @@ -0,0 +1,4 @@ +--- +title: Add online terminal support for Kubernetes +merge_request: 7690 +author: -- cgit v1.2.1 From d2212a8b5f2ebc25ab8a007aa09a728779dd9212 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 19 Dec 2016 19:18:16 +0000 Subject: Add online terminal documentation --- doc/README.md | 1 + .../high_availability/load_balancer.md | 26 +++++--- doc/administration/integration/terminal.md | 73 +++++++++++++++++++++ doc/ci/environments.md | 45 ++++++++++++- .../img/environments_terminal_button_on_index.png | Bin 0 -> 79725 bytes .../img/environments_terminal_button_on_show.png | Bin 0 -> 73210 bytes doc/ci/img/environments_terminal_page.png | Bin 0 -> 117863 bytes doc/project_services/kubernetes.md | 14 ++++ doc/user/permissions.md | 1 + 9 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 doc/administration/integration/terminal.md create mode 100644 doc/ci/img/environments_terminal_button_on_index.png create mode 100644 doc/ci/img/environments_terminal_button_on_show.png create mode 100644 doc/ci/img/environments_terminal_page.png diff --git a/doc/README.md b/doc/README.md index a60a5359540..8bf33cad5e4 100644 --- a/doc/README.md +++ b/doc/README.md @@ -34,6 +34,7 @@ - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. +- [Online terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. - [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 136f570ac27..e61ea359a6a 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -10,11 +10,11 @@ you need to use with GitLab. ## Basic ports -| LB Port | Backend Port | Protocol | -| ------- | ------------ | -------- | -| 80 | 80 | HTTP | -| 443 | 443 | HTTPS [^1] | -| 22 | 22 | TCP | +| LB Port | Backend Port | Protocol | +| ------- | ------------ | --------------- | +| 80 | 80 | HTTP [^1] | +| 443 | 443 | HTTPS [^1] [^2] | +| 22 | 22 | TCP | ## GitLab Pages Ports @@ -25,8 +25,8 @@ GitLab Pages requires a separate VIP. Configure DNS to point the | LB Port | Backend Port | Protocol | | ------- | ------------ | -------- | -| 80 | Varies [^2] | HTTP | -| 443 | Varies [^2] | TCP [^3] | +| 80 | Varies [^3] | HTTP | +| 443 | Varies [^3] | TCP [^4] | ## Alternate SSH Port @@ -50,13 +50,19 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) -[^1]: When using HTTPS protocol for port 443, you will need to add an SSL +[^1]: [Terminal support](../../ci/environments.md#terminal-support) requires + your load balancer to correctly handle WebSocket connections. When using + HTTP or HTTPS proxying, this means your load balancer must be configured + to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the + [online terminal](../integration/terminal.md) integration guide for + more details. +[^2]: When using HTTPS protocol for port 443, you will need to add an SSL certificate to the load balancers. If you wish to terminate SSL at the GitLab application server instead, use TCP protocol. -[^2]: The backend port for GitLab Pages depends on the +[^3]: The backend port for GitLab Pages depends on the `gitlab_pages['external_http']` and `gitlab_pages['external_https']` setting. See [GitLab Pages documentation][gitlab-pages] for more details. -[^3]: Port 443 for GitLab Pages should always use the TCP protocol. Users can +[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md new file mode 100644 index 00000000000..05d0a97e554 --- /dev/null +++ b/doc/administration/integration/terminal.md @@ -0,0 +1,73 @@ +# Online terminals + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) +in GitLab 8.15. Only project masters and owners can access online terminals. + +With the introduction of the [Kubernetes](../../project_services/kubernetes.md) +project service, GitLab gained the ability to store and use credentials for a +Kubernetes cluster. One of the things it uses these credentials for is providing +access to [online terminals](../../ci/environments.html#online-terminals) +for environments. + +## How it works + +A detailed overview of the architecture of online terminals and how they work +can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md). +In brief: + +* GitLab relies on the user to provide their own Kubernetes credentials, and to + appropriately label the pods they create when deploying. +* When a user navigates to the terminal page for an environment, they are served + a JavaScript application that opens a WebSocket connection back to GitLab. +* The WebSocket is handled in [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse), + rather than the Rails application server. +* Workhorse queries Rails for connection details and user permissions; Rails + queries Kubernetes for them in the background, using [Sidekiq](../troubleshooting/sidekiq.md) +* Workhorse acts as a proxy server between the user's browser and the Kubernetes + API, passing WebSocket frames between the two. +* Workhorse regularly polls Rails, terminating the WebSocket connection if the + user no longer has permission to access the terminal, or if the connection + details have changed. + +## Enabling and disabling terminal support + +As online terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of +Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers +through to the next one in the chain. If you installed Gitlab using Omnibus, or +from source, starting with GitLab 8.15, this should be done by the default +configuration, so there's no need for you to do anything. + +However, if you run a [load balancer](../high_availability/load_balancer.md) in +front of GitLab, you may need to make some changes to your configuration. These +guides document the necessary steps for a selection of popular reverse proxies: + +* [Apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) +* [NGINX](https://www.nginx.com/blog/websocket-nginx/) +* [HAProxy](http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/) +* [Varnish](https://www.varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html) + +Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so +it's safe to enable support for these headers globally. If you'd rather had a +narrower set of rules, you can restrict it to URLs ending with `/terminal.ws` +(although this may still have a few false positives). + +If you installed from source, or have made any configuration changes to your +Omnibus installation before upgrading to 8.15, you may need to make some +changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration) +document for more details. + +If you'd like to disable online terminal support in GitLab, just stop passing +the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse +proxy in the chain. For most users, this will be the NGINX server bundled with +Omnibus Gitlab, in which case, you need to: + +* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file +* Ensure the whole block is uncommented, and then comment out or remove the + `Connection` and `Upgrade` lines. + +For your own load balancer, just reverse the configuration changes recommended +by the above guides. + +When these headers are not passed through, Workhorse will return a +`400 Bad Request` response to users attempting to use an online terminal. In +turn, they will receive a `Connection failed` message. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index bad0233a9ce..07d92bb746c 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -25,7 +25,9 @@ Environments are like tags for your CI jobs, describing where code gets deployed Deployments are created when [jobs] deploy versions of code to environments, so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your -servers. +servers. If you have a deployment service such as [Kubernetes][kubernetes-service] +enabled for your project, you can use it to assist with your deployments, and +can even access a terminal for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -233,6 +235,46 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! +## Terminal support + +>**Note:** +Terminal support was added in GitLab 8.15 and is only available to project +masters and owners. + +If you deploy to your environments with the help of a deployment service (e.g., +the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open +a terminal session to your environment! This is a very powerful feature that +allows you to debug issues without leaving the comfort of your web browser. To +enable it, just follow the instructions given in the service documentation. + +Once enabled, your environments will gain a "terminal" button: + +![Terminal button on environment index](img/environments_terminal_button_on_index.png) + +You can also access the terminal button from the page for a specific environment: + +![Terminal button for an environment](img/environments_terminal_button_on_show.png) + +Wherever you find it, clicking the button will take you to a separate page to +establish the terminal session: + +![Terminal page](img/environments_terminal_page.png) + +This works just like any other terminal - you'll be in the container created +by your deployment, so you can run shell commands and get responses in real +time, check the logs, try out configuration or code tweaks, etc. You can open +multiple terminals to the same environment - they each get their own shell +session - and even a multiplexer like `screen` or `tmux`! + +>**Note:** +Container-based deployments often lack basic tools (like an editor), and may +be stopped or restarted at any time. If this happens, you will lose all your +changes! Treat this as a debugging tool, not a comprehensive online IDE. You +can use [Koding](../administration/integration/koding.md) for online +development. + +--- + While this is fine for deploying to some stable environments like staging or production, what happens for branches? So far we haven't defined anything regarding deployments for branches other than `master`. Dynamic environments @@ -524,6 +566,7 @@ Below are some links you may find interesting: [Pipelines]: pipelines.md [jobs]: yaml/README.md#jobs [yaml]: yaml/README.md +[kubernetes-service]: ../project_services/kubernetes.md] [environments]: #environments [deployments]: #deployments [permissions]: ../user/permissions.md diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png new file mode 100644 index 00000000000..6f05b2aa343 Binary files /dev/null and b/doc/ci/img/environments_terminal_button_on_index.png differ diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png new file mode 100644 index 00000000000..9469fab99ab Binary files /dev/null and b/doc/ci/img/environments_terminal_button_on_show.png differ diff --git a/doc/ci/img/environments_terminal_page.png b/doc/ci/img/environments_terminal_page.png new file mode 100644 index 00000000000..fde1bf325a6 Binary files /dev/null and b/doc/ci/img/environments_terminal_page.png differ diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index fda364b864e..0c5c88dd983 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -47,3 +47,17 @@ GitLab CI build environment: - `KUBE_TOKEN` - `KUBE_NAMESPACE` - `KUBE_CA_PEM` - only if a custom CA bundle was specified + +## Terminal support + +>**NOTE:** +Added in GitLab 8.15. You must be the project owner or have `master` permissions +to use terminals. Support is currently limited to the first container in the +first pod of your environment. + +When enabled, the Kubernetes service adds online [terminal support](../ci/environments.md#terminal-support) +to your environments. This is based on the `exec` functionality found in +Docker and Kubernetes, so you get a new shell session within your existing +containers. To use this integration, you should deploy to Kubernetes using +the deployment variables above, ensuring any pods you create are labelled with +`app=$CI_ENVIRONMENT_SLUG`. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 39fe2409a29..5ada8748d85 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -33,6 +33,7 @@ The following table depicts the various user permission levels in a project. | See a container registry | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ | +| Use environment terminals | | | | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | Manage/Accept merge requests | | | ✓ | ✓ | ✓ | -- cgit v1.2.1 From 34e317d21ce49c122beb73d2eb695311b75d0b89 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 19 Dec 2016 17:58:46 +0000 Subject: Adds loading to improve UX --- .../mini_pipeline_graph_dropdown.js.es6 | 42 ++++++++++++++++------ app/assets/stylesheets/pages/pipelines.scss | 12 +++++-- .../projects/ci/pipelines/_pipeline.html.haml | 10 ++++-- app/views/projects/pipelines/_stage.html.haml | 10 +++--- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index ce24cbdb705..1db1ad6f017 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -18,31 +18,34 @@ class MiniPipelineGraph { constructor({ container }) { this.container = container; + this.dropdownListSelector = '.js-builds-dropdown-container'; this.getBuildsList = this.getBuildsList.bind(this); this.bindEvents(); } /** - * Adds an removes the event listener. - * TODO: Remove jQuery when we have a way to handle events properly. + * Adds and removes the event listener. */ bindEvents() { - $(this.container).off('click', 'button.js-builds-dropdown-button', this.getBuildsList); - $(this.container).on('click', 'button.js-builds-dropdown-button', this.getBuildsList); + const dropdownButtonSelector = 'button.js-builds-dropdown-button'; + + $(this.container).off('click', dropdownButtonSelector, this.getBuildsList); + $(this.container).on('click', dropdownButtonSelector, this.getBuildsList); } /** - * For the clicked stage, renders the received html in the sibiling - * element with the `js-builds-dropdown-container` clas + * For the clicked stage, renders the given data in the dropdown list. * - * @param {Element} stageContainer + * @param {HTMLElement} stageContainer * @param {Object} data */ renderBuildsList(stageContainer, data) { - const dropdownContainer = stageContainer.parentElement.querySelector('.js-builds-dropdown-container'); + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); - dropdownContainer.innerHTML = data.html; + dropdownContainer.innerHTML = data; } /** @@ -58,10 +61,29 @@ dataType: 'json', type: 'GET', url: endpoint, - success: data => this.renderBuildsList(e.currentTarget, data), + beforeSend: () => { + this.renderBuildsList(e.currentTarget, ''); + this.toggleLoading(e.currentTarget); + }, + success: (data) => { + this.toggleLoading(e.currentTarget); + this.renderBuildsList(e.currentTarget, data.html); + }, error: () => new Flash('An error occurred while fetching the builds.', 'alert'), }); } + + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } } window.gl = window.gl || {}; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b3deac3ab75..ae7b40a9416 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -753,13 +753,19 @@ margin: 0; } + .builds-dropdown-loading { + margin: 10px auto; + width: 18px; + } + .grouped-pipeline-dropdown { right: -172px; top: 23px; - } + min-height: 191px; - .grouped-pipeline-dropdown a { - color: $gl-text-color-light; + a { + color: $gl-text-color-light; + } } .arrow-up { diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index d488eeda2fe..6dfc55aa23c 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -53,13 +53,19 @@ .stage-container.mini-pipeline-graph - if hasMultipleBuilds .dropdown.inline.build-content - %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} + %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} %span{ class: klass } %span.mini-pipeline-graph-icon-container %span{ class: icon_status_klass }= custom_icon(icon_status) = icon('caret-down', class: 'dropdown-caret') - %div.js-builds-dropdown-container + .js-builds-dropdown-container + .dropdown-menu.grouped-pipeline-dropdown + .arrow-up + .js-builds-dropdown-list + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin - else - if detailed_status.has_details? = link_to detailed_status.details_path, class: klass, title: tooltip do diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index 83fd518726d..20456e792e7 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,6 +1,4 @@ -.dropdown-menu.grouped-pipeline-dropdown - .arrow-up - %ul - - @stage.statuses.each do |status| - %li.dropdown-build - = render 'ci/status/graph_badge', subject: status +%ul + - @stage.statuses.each do |status| + %li.dropdown-build + = render 'ci/status/graph_badge', subject: status -- cgit v1.2.1 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: 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 } -- cgit v1.2.1 From 34295036e2a9ecf18ca5440a5dd6dbb0c7f05643 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 19 Dec 2016 23:53:19 +0100 Subject: Improve sources - Add proper error handling, - Use flash[:alert] and flash[:notice], - Use `resource` instead of `resources`, --- app/controllers/projects/mattermost_controller.rb | 39 -------------------- app/controllers/projects/mattermosts_controller.rb | 41 ++++++++++++++++++++++ app/helpers/projects_helper.rb | 9 +++-- .../mattermost_slash_commands_service.rb | 35 ++++++------------ app/views/projects/mattermost/_no_teams.html.haml | 12 ------- .../projects/mattermost/_team_selection.html.haml | 41 ---------------------- app/views/projects/mattermost/new.html.haml | 8 ----- app/views/projects/mattermosts/_no_teams.html.haml | 12 +++++++ .../projects/mattermosts/_team_selection.html.haml | 41 ++++++++++++++++++++++ app/views/projects/mattermosts/new.html.haml | 8 +++++ .../_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 +++++- 15 files changed, 152 insertions(+), 153 deletions(-) delete mode 100644 app/controllers/projects/mattermost_controller.rb create mode 100644 app/controllers/projects/mattermosts_controller.rb delete mode 100644 app/views/projects/mattermost/_no_teams.html.haml delete mode 100644 app/views/projects/mattermost/_team_selection.html.haml delete mode 100644 app/views/projects/mattermost/new.html.haml create mode 100644 app/views/projects/mattermosts/_no_teams.html.haml create mode 100644 app/views/projects/mattermosts/_team_selection.html.haml create mode 100644 app/views/projects/mattermosts/new.html.haml 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/mattermost/_no_teams.html.haml deleted file mode 100644 index 605c7f61dee..00000000000 --- a/app/views/projects/mattermost/_no_teams.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%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 deleted file mode 100644 index e0ab63dbc5d..00000000000 --- a/app/views/projects/mattermost/_team_selection.html.haml +++ /dev/null @@ -1,41 +0,0 @@ -%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 deleted file mode 100644 index 96b1d2aee61..00000000000 --- a/app/views/projects/mattermost/new.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.service-installation - .inline.pull-right - = custom_icon('mattermost_logo', size: 48) - %h3 Install Mattermost Command - - if @teams.empty? - = render 'no_teams' - - else - = render 'team_selection' diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml new file mode 100644 index 00000000000..605c7f61dee --- /dev/null +++ b/app/views/projects/mattermosts/_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/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml new file mode 100644 index 00000000000..376592e66c9 --- /dev/null +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -0,0 +1,41 @@ +%p + This service will be installed on the Mattermost instance at + %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host +%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 + - 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 @teams.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/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml new file mode 100644 index 00000000000..96b1d2aee61 --- /dev/null +++ b/app/views/projects/mattermosts/new.html.haml @@ -0,0 +1,8 @@ +.service-installation + .inline.pull-right + = custom_icon('mattermost_logo', size: 48) + %h3 Install Mattermost Command + - if @teams.empty? + = render 'no_teams' + - else + = 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 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 -- cgit v1.2.1 From 841960f847f04da9c427bcdb19037e2112a90890 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 00:22:10 +0100 Subject: Fix flow --- app/controllers/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 -- cgit v1.2.1 From f7b7e918fef6567d26e7fe17894e5df14c58f37c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 00:29:38 +0100 Subject: Remove rest of the form parameters as we can't really support them --- .../services/mattermost_slash_commands/_help.html.haml | 7 +++++-- .../mattermost_slash_commands/_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 -- cgit v1.2.1 From 9edef45ae169e5882dab4950a8143acf7623a06d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 19 Dec 2016 23:44:34 +0000 Subject: Resolve conflict Fix tooltips in dropdown --- app/assets/stylesheets/pages/pipelines.scss | 75 ++--------------------------- app/views/ci/status/_graph_badge.html.haml | 12 ++--- 2 files changed, 10 insertions(+), 77 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bb320da619f..832d8443dc8 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -463,15 +463,11 @@ white-space: normal; color: $gl-text-color-light; -<<<<<<< HEAD - .dropdown-menu-toggle { background-color: transparent; border: none; padding: 0; color: $gl-text-color-light; - flex-grow: 1; - &:focus { outline: none; @@ -486,10 +482,6 @@ } } - &:hover { - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-border; -======= > .build-content { display: inline-block; padding: 8px 10px 9px; @@ -497,8 +489,7 @@ border: 1px solid $border-color; border-radius: 30px; background-color: $white-light; ->>>>>>> master - + &:hover { background-color: $stage-hover-bg; border: 1px solid $stage-hover-border; @@ -603,22 +594,7 @@ } } } - - // Position in the pipeline graph - .grouped-pipeline-dropdown { -<<<<<<< HEAD - right: -206px; - top: -11px; - } } -======= - padding: 0; - width: 191px; - left: auto; - right: -195px; - top: -4px; - box-shadow: 0 1px 5px $black-transparent; ->>>>>>> master .dropdown-counter-badge { float: right; @@ -632,8 +608,8 @@ padding: 0; width: 191px; left: auto; - right: -206px; - top: -11px; + right: -195px; + top: -4px; box-shadow: 0 1px 5px $black-transparent; a { @@ -644,53 +620,10 @@ } } -<<<<<<< HEAD ul { max-height: 245px; overflow: auto; margin: 5px 0; -======= - .dropdown-build { - color: $gl-text-color-light; - - .build-content { - width: 100%; - } - - .ci-action-icon-container { - font-size: 11px; - position: absolute; - right: 4px; - - i { - width: 25px; - height: 25px; - font-size: 11px; - margin-top: 0; - - &::before { - top: 1px; - left: 1px; - } - } - } - - &:hover { - background-color: $stage-hover-bg; - border-radius: 3px; - color: $gl-text-color; - } - - .stage { - max-width: 100px; - width: 100px; - } - - .ci-status-icon svg { - height: 18px; - width: 18px; - } ->>>>>>> master li { padding-top: 2px; @@ -747,7 +680,7 @@ .dropdown-build { color: $gl-text-color-light; - a.ci-action-icon-container { + .ci-action-icon-container { padding: 0; font-size: 11px; float: right; diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index 52b4d77d074..dd2f649de9a 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -3,18 +3,18 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) - klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" - if status.has_details? - = link_to status.details_path, class: 'build-content' do + = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do %span{ class: klass }= custom_icon(status.icon) - .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name + .ci-status-text= subject.name - else - .build-content + .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } } %span{ class: klass }= custom_icon(status.icon) - .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name + .ci-status-text= subject.name - if status.has_action? - = link_to status.action_path, method: status.action_method, - title: status.action_title, class: 'ci-action-icon-container' do + = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do %i.ci-action-icon-wrapper = icon(status.action_icon, class: status.action_class) -- cgit v1.2.1 From ac86c495a3fc54be6984c4df2b363e9b4e414b4d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 19 Dec 2016 23:45:24 +0000 Subject: Pipeline graph node was one pixel off --- 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 832d8443dc8..3639db026cf 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -489,7 +489,7 @@ border: 1px solid $border-color; border-radius: 30px; background-color: $white-light; - + &:hover { background-color: $stage-hover-bg; border: 1px solid $stage-hover-border; @@ -547,7 +547,7 @@ content: ''; position: absolute; top: 48%; - right: -49px; + right: -48px; border-top: 2px solid $border-color; width: 48px; height: 1px; -- cgit v1.2.1 From 8275efa36bd51ad0d46495341d524b215c01bfff Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 19 Dec 2016 19:48:49 -0200 Subject: Fix member with expiration date feature spec --- .../projects/members/master_adds_member_with_expiration_date_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 27a83fdcd1f..b7273021c95 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -24,7 +24,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: click_on 'Add to project' end - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do expect(page).to have_content('Expires in 4 days') end end @@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') visit namespace_project_project_members_path(project.namespace, project) - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do find('.js-access-expiration-date').set '2016-08-09' wait_for_ajax expect(page).to have_content('Expires in 3 days') -- cgit v1.2.1 From 212967aefb55e0792a36e67a881b66c8bd871e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 20 Dec 2016 09:45:37 +0100 Subject: Reject blank environment vcariables in Gitlab::Git::RevList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/git/rev_list.rb | 4 ++-- spec/lib/gitlab/git/rev_list_spec.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 25e9d619697..79dd0cf7df2 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -22,7 +22,7 @@ module Gitlab def valid? environment_variables.all? do |(name, value)| - value.start_with?(project.repository.path_to_repo) + value.to_s.start_with?(project.repository.path_to_repo) end end @@ -35,7 +35,7 @@ module Gitlab end def environment_variables - @environment_variables ||= env.slice(*ALLOWED_VARIABLES) + @environment_variables ||= env.slice(*ALLOWED_VARIABLES).compact end end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 444639acbaa..1f9c987be0b 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -26,6 +26,13 @@ describe Gitlab::Git::RevList, lib: true do expect(rev_list).not_to be_valid end + + it "ignores nil values" do + env = { var => nil } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end end end end -- cgit v1.2.1 From 2b0b53cddd7d57ca5dd93437fdffefd7a07af91e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 11:00:56 +0100 Subject: Add tests for stage API endpoint --- app/controllers/projects/pipelines_controller.rb | 4 +-- app/models/ci/pipeline.rb | 5 ++++ app/models/ci/stage.rb | 4 +++ spec/features/projects/pipelines/pipelines_spec.rb | 29 ++++++++++++++++++++++ spec/models/ci/pipeline_spec.rb | 20 +++++++++++++++ spec/models/ci/stage_spec.rb | 11 ++++++++ 6 files changed, 70 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 0147072b0f1..cc347922c6a 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -42,9 +42,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def stage - @stage = pipeline.stages.find do |stage| - stage.name == params[:stage] - end + @stage = pipeline.stage(params[:stage]) return not_found unless @stage respond_to do |format| diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 48354cdbefb..f2f6453b3b9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -116,6 +116,11 @@ module Ci where.not(duration: nil).sum(:duration) end + def stage(name) + stage = Ci::Stage.new(self, name: name) + stage unless stage.statuses_count.zero? + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 7ef59445d77..d035eda6df5 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -18,6 +18,10 @@ module Ci name end + def statuses_count + @statuses_count ||= statuses.count + end + def status @status ||= statuses.latest.status end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index f3731698a18..e1c6b4c115c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -152,6 +152,35 @@ describe "Pipelines" do end end + describe 'GET /:project/pipelines/stage?name=stage' do + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, ref: 'master', + status: 'running') + end + + context 'when accessing existing stage' do + let!(:build) do + create(:ci_build, pipeline: pipeline, stage: 'build') + end + + before do + visit stage_namespace_project_pipeline_path( + project.namespace, project, pipeline, format: :json, stage: 'build') + end + + it { expect(page).to have_http_status(:ok) } + end + + context 'when accessing unknown stage' do + before do + visit stage_namespace_project_pipeline_path( + project.namespace, project, pipeline, format: :json, stage: 'test') + end + + it { expect(page).to have_http_status(:not_found) } + end + end + describe 'POST /:project/pipelines' do let(:project) { create(:project) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 52dd41065e9..67cc3e6be68 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -175,6 +175,26 @@ describe Ci::Pipeline, models: true do end end + describe '#stage' do + subject { pipeline.stage('test') } + + context 'with status in stage' do + let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'test') } + + it 'return stage object' do + is_expected.to be_a(Ci::Stage) + end + end + + context 'without status in stage' do + let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'build') } + + it 'return stage object' do + is_expected.to be_nil + end + end + end + describe 'state machine' do let(:current) { Time.now.change(usec: 0) } let(:build) { create_build('build1', 0) } diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 8fff38f7cda..d8dce0f1cc6 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -28,6 +28,17 @@ describe Ci::Stage, models: true do end end + describe '#statuses_count' do + let!(:stage_build) { create_job(:ci_build) } + let!(:other_build) { create_job(:ci_build, stage: 'other stage') } + + subject { stage.statuses_count } + + it "statuses only from current stage" do + is_expected.to eq(1) + end + end + describe '#builds' do let!(:stage_build) { create_job(:ci_build) } let!(:commit_status) { create_job(:commit_status) } -- cgit v1.2.1 From 5ec1c140d991b37d665c47e52dba4a453cc305a4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 11:26:24 +0100 Subject: Improve specs --- spec/features/projects/pipelines/pipelines_spec.rb | 7 +++++-- spec/models/ci/pipeline_spec.rb | 14 +++++++++----- spec/models/ci/stage_spec.rb | 6 ++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index e1c6b4c115c..24e501b0151 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -152,7 +152,7 @@ describe "Pipelines" do end end - describe 'GET /:project/pipelines/stage?name=stage' do + describe 'GET /:project/pipelines/stage.json?name=stage' do let!(:pipeline) do create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') @@ -168,7 +168,10 @@ describe "Pipelines" do project.namespace, project, pipeline, format: :json, stage: 'build') end - it { expect(page).to have_http_status(:ok) } + it do + expect(page).to have_http_status(:ok) + expect(JSON.parse(page.source)).to include("html") + end end context 'when accessing unknown stage' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 67cc3e6be68..5e1a9fa8dd8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -179,15 +179,19 @@ describe Ci::Pipeline, models: true do subject { pipeline.stage('test') } context 'with status in stage' do - let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'test') } - - it 'return stage object' do - is_expected.to be_a(Ci::Stage) + before do + create(:commit_status, pipeline: pipeline, stage: 'test') end + + it { expect(subject).to be_a(Ci::Stage) } + it { expect(subject.name).to eq('stage') } + it { expect(subject.statues).not_to be_empty } end context 'without status in stage' do - let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'build') } + before do + create(:commit_status, pipeline: pipeline, stage: 'build') + end it 'return stage object' do is_expected.to be_nil diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index d8dce0f1cc6..65a302e9d9b 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -29,8 +29,10 @@ describe Ci::Stage, models: true do end describe '#statuses_count' do - let!(:stage_build) { create_job(:ci_build) } - let!(:other_build) { create_job(:ci_build, stage: 'other stage') } + before do + create_job(:ci_build) } + create_job(:ci_build, stage: 'other stage') + end subject { stage.statuses_count } -- cgit v1.2.1 From 51353a6bc7257612412c8712230c0d47667da021 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 11:27:24 +0100 Subject: Always show dropdown --- .../projects/ci/pipelines/_pipeline.html.haml | 31 +++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 6dfc55aa23c..ce8b9f9e8af 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -45,32 +45,25 @@ - if stage.status - detailed_status = stage.detailed_status(current_user) - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status.group}" - - hasMultipleBuilds = stage.statuses.count > 1 - icon_status = "#{detailed_status.icon}_borderless" - icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - tooltip = "#{stage.name}: #{detailed_status.label || 'not found'}" .stage-container.mini-pipeline-graph - - if hasMultipleBuilds - .dropdown.inline.build-content - %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} - %span{ class: klass } - %span.mini-pipeline-graph-icon-container - %span{ class: icon_status_klass }= custom_icon(icon_status) - = icon('caret-down', class: 'dropdown-caret') - - .js-builds-dropdown-container - .dropdown-menu.grouped-pipeline-dropdown - .arrow-up - .js-builds-dropdown-list - - .js-builds-dropdown-loading.builds-dropdown-loading.hidden - %span.fa.fa-spinner.fa-spin - - else - - if detailed_status.has_details? - = link_to detailed_status.details_path, class: klass, title: tooltip do + .dropdown.inline.build-content + %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} + %span{ class: klass } %span.mini-pipeline-graph-icon-container %span{ class: icon_status_klass }= custom_icon(icon_status) + = icon('caret-down', class: 'dropdown-caret') + + .js-builds-dropdown-container + .dropdown-menu.grouped-pipeline-dropdown + .arrow-up + .js-builds-dropdown-list + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin %td - if pipeline.duration -- cgit v1.2.1 From d305d15b6dde99b0de7fc78242aaa095fc79b5ca Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 11:32:42 +0100 Subject: 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 = {}) -- cgit v1.2.1 From 0cf23fde7c666b64e6c18a92d29e632f51b00059 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 12:02:37 +0100 Subject: 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 -- cgit v1.2.1 From ff31f4f922e2b35f20dc4d2ba8be951d95c887be Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 20 Dec 2016 11:05:39 +0000 Subject: Rename "Online terminal" to "Web terminal" in the docs --- doc/README.md | 2 +- doc/administration/high_availability/load_balancer.md | 4 ++-- doc/administration/integration/terminal.md | 16 ++++++++-------- doc/ci/environments.md | 6 +++--- doc/project_services/kubernetes.md | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/README.md b/doc/README.md index 8bf33cad5e4..ee69684b53b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -34,7 +34,7 @@ - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. -- [Online terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. +- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. - [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index e61ea359a6a..1824829903c 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -50,11 +50,11 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) -[^1]: [Terminal support](../../ci/environments.md#terminal-support) requires +[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires your load balancer to correctly handle WebSocket connections. When using HTTP or HTTPS proxying, this means your load balancer must be configured to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the - [online terminal](../integration/terminal.md) integration guide for + [web terminal](../integration/terminal.md) integration guide for more details. [^2]: When using HTTPS protocol for port 443, you will need to add an SSL certificate to the load balancers. If you wish to terminate SSL at the diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 05d0a97e554..a1d1bb03b50 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,17 +1,17 @@ -# Online terminals +# Web terminals > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) -in GitLab 8.15. Only project masters and owners can access online terminals. +in GitLab 8.15. Only project masters and owners can access web terminals. With the introduction of the [Kubernetes](../../project_services/kubernetes.md) project service, GitLab gained the ability to store and use credentials for a Kubernetes cluster. One of the things it uses these credentials for is providing -access to [online terminals](../../ci/environments.html#online-terminals) +access to [web terminals](../../ci/environments.html#web-terminals) for environments. ## How it works -A detailed overview of the architecture of online terminals and how they work +A detailed overview of the architecture of web terminals and how they work can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md). In brief: @@ -31,7 +31,7 @@ In brief: ## Enabling and disabling terminal support -As online terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of +As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers through to the next one in the chain. If you installed Gitlab using Omnibus, or from source, starting with GitLab 8.15, this should be done by the default @@ -56,7 +56,7 @@ Omnibus installation before upgrading to 8.15, you may need to make some changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration) document for more details. -If you'd like to disable online terminal support in GitLab, just stop passing +If you'd like to disable web terminal support in GitLab, just stop passing the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse proxy in the chain. For most users, this will be the NGINX server bundled with Omnibus Gitlab, in which case, you need to: @@ -69,5 +69,5 @@ For your own load balancer, just reverse the configuration changes recommended by the above guides. When these headers are not passed through, Workhorse will return a -`400 Bad Request` response to users attempting to use an online terminal. In -turn, they will receive a `Connection failed` message. +`400 Bad Request` response to users attempting to use a web terminal. In turn, +they will receive a `Connection failed` message. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 07d92bb746c..98cd29c9567 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -27,7 +27,7 @@ so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your servers. If you have a deployment service such as [Kubernetes][kubernetes-service] enabled for your project, you can use it to assist with your deployments, and -can even access a terminal for your environment from within GitLab! +can even access a web terminal for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -235,10 +235,10 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! -## Terminal support +## Web terminals >**Note:** -Terminal support was added in GitLab 8.15 and is only available to project +Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index 0c5c88dd983..59d5da702f8 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -48,16 +48,16 @@ GitLab CI build environment: - `KUBE_NAMESPACE` - `KUBE_CA_PEM` - only if a custom CA bundle was specified -## Terminal support +## Web terminals >**NOTE:** Added in GitLab 8.15. You must be the project owner or have `master` permissions to use terminals. Support is currently limited to the first container in the first pod of your environment. -When enabled, the Kubernetes service adds online [terminal support](../ci/environments.md#terminal-support) -to your environments. This is based on the `exec` functionality found in +When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +support to your environments. This is based on the `exec` functionality found in Docker and Kubernetes, so you get a new shell session within your existing containers. To use this integration, you should deploy to Kubernetes using the deployment variables above, ensuring any pods you create are labelled with -`app=$CI_ENVIRONMENT_SLUG`. +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! -- cgit v1.2.1 From 11040589c803410837d67fb481221e9d6ef1d969 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 20 Dec 2016 10:08:15 +0000 Subject: Adds tests for the MiniPipelineGraph class --- .../mini_pipeline_graph_dropdown.js.es6 | 6 ++- .../fixtures/mini_dropdown_graph.html.haml | 8 ++++ .../mini_pipeline_graph_dropdown_spec.js.es6 | 51 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 spec/javascripts/fixtures/mini_dropdown_graph.html.haml create mode 100644 spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 1db1ad6f017..73d48074e41 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -16,8 +16,8 @@ */ (() => { class MiniPipelineGraph { - constructor({ container }) { - this.container = container; + constructor(opts = {}) { + this.container = opts.container || ''; this.dropdownListSelector = '.js-builds-dropdown-container'; this.getBuildsList = this.getBuildsList.bind(this); @@ -57,6 +57,8 @@ getBuildsList(e) { const endpoint = e.currentTarget.dataset.stageEndpoint; + console.log('ENDPOINT', endpoint); + return $.ajax({ dataType: 'json', type: 'GET', diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml new file mode 100644 index 00000000000..e9bf7568e95 --- /dev/null +++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml @@ -0,0 +1,8 @@ +%div.js-builds-dropdown-tests + %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'} + Dropdown + %div.js-builds-dropdown-container + %div.js-builds-dropdown-list + + %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 new file mode 100644 index 00000000000..d1793e9308e --- /dev/null +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -0,0 +1,51 @@ +/* eslint-disable no-new */ + +//= require flash +//= require mini_pipeline_graph_dropdown + +(() => { + describe('Mini Pipeline Graph Dropdown', () => { + fixture.preload('mini_dropdown_graph'); + + beforeEach(() => { + fixture.load('mini_dropdown_graph'); + }); + + describe('When is initialized', () => { + it('should initialize without errors when no options are given', () => { + const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + + expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); + }); + + it('should set the container as the given prop', () => { + const container = '.foo'; + + const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + + expect(miniPipelineGraph.container).toEqual(container); + }); + }); + + describe('When dropdown is clicked', () => { + it('should call getBuildsList', () => { + const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + + expect(getBuildsListSpy).toHaveBeenCalled(); + }); + + it('should make a request to the endpoint provided in the html', () => { + const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + }); + }); + }); +})(); -- cgit v1.2.1 From dd5965a15ad3fcb0f411b25e2079ef20f311e446 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 20 Dec 2016 11:14:46 +0000 Subject: Remove console.log --- app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 73d48074e41..31b65a22723 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -57,8 +57,6 @@ getBuildsList(e) { const endpoint = e.currentTarget.dataset.stageEndpoint; - console.log('ENDPOINT', endpoint); - return $.ajax({ dataType: 'json', type: 'GET', -- cgit v1.2.1 From c87d93d462bb83caebd22bd759d8a1ead845d6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 19 Dec 2016 16:26:59 +0100 Subject: Improve specs for Repositories API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../unreleased/4269-public-repositories-api.yml | 2 +- spec/requests/api/repositories_spec.rb | 336 ++++++++++++++------- spec/support/api/status_shared_examples.rb | 42 +++ 3 files changed, 266 insertions(+), 114 deletions(-) create mode 100644 spec/support/api/status_shared_examples.rb diff --git a/changelogs/unreleased/4269-public-repositories-api.yml b/changelogs/unreleased/4269-public-repositories-api.yml index b88ce63845d..861307a022b 100644 --- a/changelogs/unreleased/4269-public-repositories-api.yml +++ b/changelogs/unreleased/4269-public-repositories-api.yml @@ -1,4 +1,4 @@ --- title: Allow Repositories API GET endpoints to be requested anonymously -merge_request: +merge_request: 8148 author: diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 67f0bc537fe..fe28ad1d1a1 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -7,174 +7,239 @@ describe API::Repositories, api: true do include WorkhorseHelpers let(:user) { create(:user) } - let(:user2) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:project) { create(:project, creator_id: user.id) } let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + + shared_context 'disabled repository' do + before do + project.project_feature.update_attributes!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + expect(project.feature_available?(:repository, current_user)).to be false + end + end describe "GET /projects/:id/repository/tree" do - context "authorized user" do - before { project.team << [user2, :reporter] } + let(:route) { "/projects/#{project.id}/repository/tree" } - shared_examples_for 'repository tree' do - it 'returns the repository tree' do - get api("/projects/#{project.id}/repository/tree", current_user) + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get api(route, current_user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) - first_commit = json_response.first + first_commit = json_response.first - expect(json_response).to be_an Array - expect(first_commit['name']).to eq('bar') - expect(first_commit['type']).to eq('tree') - expect(first_commit['mode']).to eq('040000') - end + expect(json_response).to be_an Array + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') end - context 'when unauthenticated' do - it_behaves_like 'repository tree' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } end end - context 'when authenticated' do - it_behaves_like 'repository tree' do - let(:current_user) { user } + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } end end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) - expect(response).to have_http_status(404) + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get api("#{route}?recursive=1", current_user) - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' - end - end + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree") + context 'when repository is disabled' do + include_context 'disabled repository' - expect(response).to have_http_status(404) + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end end end - end - - describe 'GET /projects/:id/repository/tree?recursive=1' do - context 'authorized user' do - before { project.team << [user2, :reporter] } - - it 'should return recursive project paths tree' do - get api("/projects/#{project.id}/repository/tree?recursive=1", user) - expect(response.status).to eq(200) - - expect(json_response).to be_an Array - expect(json_response[4]['name']).to eq('html') - expect(json_response[4]['path']).to eq('files/html') - expect(json_response[4]['type']).to eq('tree') - expect(json_response[4]['mode']).to eq('040000') + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } end + end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user) - expect(response).to have_http_status(404) - - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } end end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree?recursive=1") + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } end end end - describe "GET /projects/:id/repository/blobs/:sha & /projects/:id/repository/commits/:sha" do - shared_examples_for 'repository blob' do - it 'returns the repository blob for /repository/blobs/master' do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", current_user) + { + 'blobs/:sha' => 'blobs/master', + 'commits/:sha/blob' => 'commits/master/blob' + }.each do |desc_path, example_path| + describe "GET /projects/:id/repository/#{desc_path}" do + let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } - expect(response).to have_http_status(200) - end + shared_examples_for 'repository blob' do + it 'returns the repository blob' do + get api(route, current_user) - it 'returns the repository blob for /repository/commits/master' do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", current_user) + expect(response).to have_http_status(200) + end - expect(response).to have_http_status(200) - end - end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:message) { '404 Commit Not Found' } + end + end - context 'when unauthenticated' do - it_behaves_like 'repository blob' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } + context 'when filepath does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) } + let(:message) { '404 File Not Found' } + end + end + + context 'when no filepath is given' do + it_behaves_like '400 response' do + let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - end - context 'when authenticated' do - it_behaves_like 'repository blob' do - let(:current_user) { user } + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - end - it "returns 404 for invalid branch_name" do - get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) - expect(response).to have_http_status(404) - end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end - it "returns 404 for invalid file" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) - expect(response).to have_http_status(404) - end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } + end + end - it "returns a 400 error if filepath is missing" do - get api("/projects/#{project.id}/repository/blobs/master", user) - expect(response).to have_http_status(400) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end + end end end describe "GET /projects/:id/repository/raw_blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + shared_examples_for 'repository raw blob' do it 'returns the repository raw blob' do - get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", current_user) + get api(route, current_user) expect(response).to have_http_status(200) end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - context 'when unauthenticated' do + context 'when unauthenticated', 'and project is public' do it_behaves_like 'repository raw blob' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end - context 'when authenticated' do + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do it_behaves_like 'repository raw blob' do let(:current_user) { user } end end - it 'returns a 404 for unknown blob' do - get api("/projects/#{project.id}/repository/raw_blobs/123456", user) - expect(response).to have_http_status(404) - - expect(json_response).to be_an Object - json_response['message'] == '404 Blob Not Found' + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe "GET /projects/:id/repository/archive(.:format)?:sha" do + let(:route) { "/projects/#{project.id}/repository/archive" } + shared_examples_for 'repository archive' do it 'returns the repository archive' do - get api("/projects/#{project.id}/repository/archive", current_user) + get api(route, current_user) expect(response).to have_http_status(200) @@ -208,31 +273,48 @@ describe API::Repositories, api: true do expect(type).to eq('git-archive') expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?sha=xxx", current_user) } + let(:message) { '404 File Not Found' } + end + end end - context 'when unauthenticated' do + context 'when unauthenticated', 'and project is public' do it_behaves_like 'repository archive' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end - context 'when authenticated' do + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do it_behaves_like 'repository archive' do let(:current_user) { user } end end - it "returns 404 for invalid sha" do - get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe 'GET /projects/:id/repository/compare' do + let(:route) { "/projects/#{project.id}/repository/compare" } + shared_examples_for 'repository compare' do it "compares branches" do - get api("/projects/#{project.id}/repository/compare", current_user), from: 'master', to: 'feature' + get api(route, current_user), from: 'master', to: 'feature' expect(response).to have_http_status(200) expect(json_response['commits']).to be_present @@ -240,7 +322,7 @@ describe API::Repositories, api: true do end it "compares tags" do - get api("/projects/#{project.id}/repository/compare", current_user), from: 'v1.0.0', to: 'v1.1.0' + get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' expect(response).to have_http_status(200) expect(json_response['commits']).to be_present @@ -248,7 +330,7 @@ describe API::Repositories, api: true do end it "compares commits" do - get api("/projects/#{project.id}/repository/compare", current_user), from: sample_commit.id, to: sample_commit.parent_id + get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id expect(response).to have_http_status(200) expect(json_response['commits']).to be_empty @@ -257,7 +339,7 @@ describe API::Repositories, api: true do end it "compares commits in reverse order" do - get api("/projects/#{project.id}/repository/compare", current_user), from: sample_commit.parent_id, to: sample_commit.id + get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id expect(response).to have_http_status(200) expect(json_response['commits']).to be_present @@ -265,7 +347,7 @@ describe API::Repositories, api: true do end it "compares same refs" do - get api("/projects/#{project.id}/repository/compare", current_user), from: 'master', to: 'master' + get api(route, current_user), from: 'master', to: 'master' expect(response).to have_http_status(200) expect(json_response['commits']).to be_empty @@ -274,24 +356,39 @@ describe API::Repositories, api: true do end end - context 'when unauthenticated' do + context 'when unauthenticated', 'and project is public' do it_behaves_like 'repository compare' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end - context 'when authenticated' do + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do it_behaves_like 'repository compare' do let(:current_user) { user } end end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end + end end describe 'GET /projects/:id/repository/contributors' do + let(:route) { "/projects/#{project.id}/repository/contributors" } + shared_examples_for 'repository contributors' do it 'returns valid data' do - get api("/projects/#{project.id}/repository/contributors", user) + get api(route, current_user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -306,17 +403,30 @@ describe API::Repositories, api: true do end end - context 'when unauthenticated' do + context 'when unauthenticated', 'and project is public' do it_behaves_like 'repository contributors' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end - context 'when authenticated' do + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do it_behaves_like 'repository contributors' do let(:current_user) { user } end end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end + end end end diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/api/status_shared_examples.rb new file mode 100644 index 00000000000..3481749a7f0 --- /dev/null +++ b/spec/support/api/status_shared_examples.rb @@ -0,0 +1,42 @@ +# Specs for status checking. +# +# Requires an API request: +# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } +shared_examples_for '400 response' do + before do + # Fires the request + request + end + + it 'returns 400' do + expect(response).to have_http_status(400) + end +end + +shared_examples_for '403 response' do + before do + # Fires the request + request + end + + it 'returns 403' do + expect(response).to have_http_status(403) + end +end + +shared_examples_for '404 response' do + let(:message) { nil } + before do + # Fires the request + request + end + + it 'returns 404' do + expect(response).to have_http_status(404) + expect(json_response).to be_an Object + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end -- cgit v1.2.1 From 27c936adf9728cd21114aa3f2f9b44deb0296eb4 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 16 Dec 2016 15:52:27 +0000 Subject: Milestoneish SQL performance partially improved and memoized Memoize Milestoneish#issues_visible_to_user and counts to reduce lookups Milstoneish SQL optimised with project, but still slow on GlobalMilestone --- app/models/concerns/milestoneish.rb | 28 +++++++++++++++++++--- app/models/global_milestone.rb | 10 +++++--- app/models/milestone.rb | 4 ++++ .../jej-memoize-milestoneish-visible-to-user.yml | 4 ++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 4359f1d7b06..8f02c226f0b 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,10 +1,15 @@ module Milestoneish def closed_items_count(user) - issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size + memoize_per_user(user, :closed_items_count) do + (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size + end end def total_items_count(user) - issues_visible_to_user(user).size + merge_requests.size + memoize_per_user(user, :total_items_count) do + issues_count = count_issues_by_state(user).values.sum + issues_count + merge_requests.size + end end def complete?(user) @@ -30,7 +35,10 @@ module Milestoneish end def issues_visible_to_user(user) - IssuesFinder.new(user).execute.where(id: issues) + memoize_per_user(user, :issues_visible_to_user) do + params = try(:project_id) ? { project_id: project_id } : {} + IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids) + end end def upcoming? @@ -50,4 +58,18 @@ module Milestoneish def expired? due_date && due_date.past? end + + private + + def count_issues_by_state(user) + memoize_per_user(user, :count_issues_by_state) do + issues_visible_to_user(user).reorder(nil).group(:state).count + end + end + + def memoize_per_user(user, method_name) + @memoized ||= {} + @memoized[method_name] ||= {} + @memoized[method_name][user.try!(:id)] ||= yield + end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index b01607dcda9..a54e478f5f8 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -24,12 +24,16 @@ class GlobalMilestone @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end + def milestoneish_ids + milestones.select(:id) + end + def safe_title @title.to_slug.normalize.to_s end def projects - @projects ||= Project.for_milestones(milestones.select(:id)) + @projects ||= Project.for_milestones(milestoneish_ids) end def state @@ -49,11 +53,11 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) + @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) + @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels) end def participants diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 45ca97adad1..0dcfec89f14 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base self.title end + def milestoneish_ids + id + end + def can_be_closed? active? && issues.opened.count.zero? end diff --git a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml new file mode 100644 index 00000000000..ab7f39a4178 --- /dev/null +++ b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml @@ -0,0 +1,4 @@ +--- +title: Milestoneish SQL performance partially improved and memoized +merge_request: 8146 +author: -- cgit v1.2.1 From a2f57f23616bc1ab7547e5a7c517cdd49b172bd8 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 19 Dec 2016 21:26:15 +0000 Subject: Fix N+1 queries on milestone show pages --- app/models/issue.rb | 2 ++ app/views/shared/milestones/_tabs.html.haml | 2 +- changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml diff --git a/app/models/issue.rb b/app/models/issue.rb index 738c96e4db3..6825553512f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 2b6ce2d7e7a..c8f2319d95a 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -21,7 +21,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants diff --git a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml new file mode 100644 index 00000000000..ad6eba3faf2 --- /dev/null +++ b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml @@ -0,0 +1,4 @@ +--- +title: Fix N+1 queries on milestone show pages +merge_request: 8185 +author: -- cgit v1.2.1 From 4b9bd188433d77fbaec8ae445716de8084b6a145 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 20 Dec 2016 13:22:31 +0000 Subject: Adds tests for the mini pipeline graph --- app/assets/stylesheets/pages/pipelines.scss | 2 +- .../projects/ci/pipelines/_pipeline.html.haml | 4 +- spec/features/projects/pipelines/pipelines_spec.rb | 50 ++++++++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 3639db026cf..fb6ddc34f1f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -748,7 +748,7 @@ .grouped-pipeline-dropdown { right: -172px; top: 23px; - min-height: 191px; + min-height: 50px; a { color: $gl-text-color-light; diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index ce8b9f9e8af..8541b351e2b 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -83,7 +83,7 @@ .btn-group.inline - if actions.any? .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -94,7 +94,7 @@ %span= build.name.humanize - if artifacts.present? .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'} = icon("download") = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 24e501b0151..d1ebb12715f 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' -describe "Pipelines" do +describe "Pipelines", feature: true, js:true do include GitlabRoutingHelper + include WaitForAjax let(:project) { create(:empty_project) } let(:user) { create(:user) } @@ -76,7 +77,11 @@ describe "Pipelines" do it { expect(page).to have_link('Manual build') } context 'when playing' do - before { click_link('Manual build') } + + before do + find('.js-pipeline-dropdown-manual-actions').click + click_link('Manual build') + end it { expect(manual.reload).to be_pending } end @@ -131,7 +136,10 @@ describe "Pipelines" do before { visit namespace_project_pipelines_path(project.namespace, project) } it { expect(page).to have_selector('.build-artifacts') } - it { expect(page).to have_link(with_artifacts.name) } + it do + find('.js-pipeline-dropdown-download').click + expect(page).to have_link(with_artifacts.name) + end end context 'with artifacts expired' do @@ -150,6 +158,42 @@ describe "Pipelines" do it { expect(page).not_to have_selector('.build-artifacts') } end end + + context 'mini pipleine graph' do + let!(:build) do + create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build') + end + + before do + visit namespace_project_pipelines_path(project.namespace, project) + end + + it 'should render a mini pipeline graph' do + endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name) + + expect(page).to have_selector('.mini-pipeline-graph') + expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']") + end + + context 'when clicking a graph stage' do + it 'should open a dropdown' do + find('.js-builds-dropdown-button').trigger('click') + + wait_for_ajax + + expect(page).to have_link build.name + end + + it 'should be possible to retry the failed build' do + find('.js-builds-dropdown-button').trigger('click') + + wait_for_ajax + + find('a.ci-action-icon-container').trigger('click') + expect(page).not_to have_content('Cancel running') + end + end + end end describe 'GET /:project/pipelines/stage.json?name=stage' do -- cgit v1.2.1 From ccfbbf7dfac61557159d743b776f145fe380527a Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 20 Dec 2016 13:53:24 +0000 Subject: 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 - 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 + allow(Gitlab.config.mattermost).to receive(:enabled).and_return(true) 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 -- cgit v1.2.1 From 3ef1e76659d0d583a2fd72d86309c2ba6816a316 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 20 Dec 2016 13:25:58 +0000 Subject: Fix broken test Changes after review --- .../javascripts/mini_pipeline_graph_dropdown.js.es6 | 21 +++++++++++++-------- app/views/projects/ci/pipelines/_pipeline.html.haml | 10 ++++------ spec/features/projects/pipelines/pipelines_spec.rb | 5 ++++- spec/models/ci/stage_spec.rb | 2 +- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 31b65a22723..90b3366f14b 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-new */ /* global Flash */ /** @@ -30,8 +31,8 @@ bindEvents() { const dropdownButtonSelector = 'button.js-builds-dropdown-button'; - $(this.container).off('click', dropdownButtonSelector, this.getBuildsList); - $(this.container).on('click', dropdownButtonSelector, this.getBuildsList); + $(this.container).off('click', dropdownButtonSelector, this.getBuildsList) + .on('click', dropdownButtonSelector, this.getBuildsList); } /** @@ -55,21 +56,25 @@ * @return {Promise} */ getBuildsList(e) { - const endpoint = e.currentTarget.dataset.stageEndpoint; + const button = e.currentTarget; + const endpoint = button.dataset.stageEndpoint; return $.ajax({ dataType: 'json', type: 'GET', url: endpoint, beforeSend: () => { - this.renderBuildsList(e.currentTarget, ''); - this.toggleLoading(e.currentTarget); + this.renderBuildsList(button, ''); + this.toggleLoading(button); }, success: (data) => { - this.toggleLoading(e.currentTarget); - this.renderBuildsList(e.currentTarget, data.html); + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); }, - error: () => new Flash('An error occurred while fetching the builds.', 'alert'), }); } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 8541b351e2b..2f8f153f9a9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -44,17 +44,15 @@ - pipeline.stages.each do |stage| - if stage.status - detailed_status = stage.detailed_status(current_user) - - klass = "has-tooltip ci-status-icon ci-status-icon-#{detailed_status.group}" - icon_status = "#{detailed_status.icon}_borderless" - - icon_status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - - tooltip = "#{stage.name}: #{detailed_status.label || 'not found'}" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" .stage-container.mini-pipeline-graph .dropdown.inline.build-content - %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: tooltip, placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} - %span{ class: klass } + %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} + %span.has-tooltip{ class: status_klass } %span.mini-pipeline-graph-icon-container - %span{ class: icon_status_klass }= custom_icon(icon_status) + %span{ class: status_klass }= custom_icon(icon_status) = icon('caret-down', class: 'dropdown-caret') .js-builds-dropdown-container diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index d1ebb12715f..5a4d14d2f95 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -74,7 +74,10 @@ describe "Pipelines", feature: true, js:true do before { visit namespace_project_pipelines_path(project.namespace, project) } - it { expect(page).to have_link('Manual build') } + it do + find('.js-pipeline-dropdown-manual-actions').click + expect(page).to have_link('Manual build') + end context 'when playing' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 65a302e9d9b..786091a577d 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -36,7 +36,7 @@ describe Ci::Stage, models: true do subject { stage.statuses_count } - it "statuses only from current stage" do + it "counts statuses only from current stage" do is_expected.to eq(1) end end -- cgit v1.2.1 From b2daf9f16892f362f57a4a0f7990dbc2f6ab8429 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 20 Dec 2016 14:17:21 +0000 Subject: Fix broken test --- spec/features/projects/pipelines/pipelines_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 5a4d14d2f95..57abbf5d7a4 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -217,7 +217,7 @@ describe "Pipelines", feature: true, js:true do it do expect(page).to have_http_status(:ok) - expect(JSON.parse(page.source)).to include("html") + expect(page.source).to include("html") end end -- cgit v1.2.1 From 8ad5d3171ac1c9079a8a828a1ee9eb16ec21fd38 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 20 Dec 2016 08:25:39 -0600 Subject: Fix sizing of avatar circles; add border --- app/assets/stylesheets/framework/avatar.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 000e591e09c..48827578d94 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -64,7 +64,7 @@ &.s32 { font-size: 20px; line-height: 30px; } &.s40 { font-size: 16px; line-height: 38px; } &.s60 { font-size: 32px; line-height: 58px; } - &.s70 { font-size: 34px; line-height: 68px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 88px; } &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 138px; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a443b6a37b3..e16a553bcda 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -93,7 +93,6 @@ .group-avatar { float: none; margin: 0 auto; - border: none; &.identicon { border-radius: 50%; -- cgit v1.2.1 From 8d3ed21f2389a3a68dac56c077bc85591bed8b0b Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 20 Dec 2016 14:54:23 +0000 Subject: 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' -- cgit v1.2.1 From 9c6480db8993ee6f1d8d1fac29e044dd00d66465 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 20 Dec 2016 15:53:53 +0100 Subject: Move test for HTML stage endpoint to controller specs --- .../projects/pipelines_controller_spec.rb | 46 ++++++++++++++++++++++ spec/features/projects/pipelines/pipeline_spec.rb | 2 +- spec/features/projects/pipelines/pipelines_spec.rb | 32 --------------- 3 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 spec/controllers/projects/pipelines_controller_spec.rb diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb new file mode 100644 index 00000000000..94113250c9f --- /dev/null +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Projects::PipelinesController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + before do + sign_in(user) + end + + describe 'GET stages.json' do + def get_stage(name) + get :stage, namespace_id: project.namespace.path, + project_id: project.path, + id: pipeline.id, + stage: name, + format: :json + end + + context 'when accessing existing stage' do + before do + create(:ci_build, pipeline: pipeline, stage: 'build') + + get_stage('build') + end + + it 'returns html source for stage dropdown' do + expect(response).to have_http_status(:ok) + expect(response).to render_template('projects/pipelines/_stage') + expect(json_response).to include('html') + end + end + + context 'when accessing unknown stage' do + before do + get_stage('test') + end + + it { expect(response).to have_http_status(:not_found) } + end + + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 1210e2745db..14e009daba8 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Pipelines", feature: true, js: true do +describe 'Pipeline', :feature, :js do include GitlabRoutingHelper let(:project) { create(:empty_project) } diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 57abbf5d7a4..fa8ba21b389 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -199,38 +199,6 @@ describe "Pipelines", feature: true, js:true do end end - describe 'GET /:project/pipelines/stage.json?name=stage' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, ref: 'master', - status: 'running') - end - - context 'when accessing existing stage' do - let!(:build) do - create(:ci_build, pipeline: pipeline, stage: 'build') - end - - before do - visit stage_namespace_project_pipeline_path( - project.namespace, project, pipeline, format: :json, stage: 'build') - end - - it do - expect(page).to have_http_status(:ok) - expect(page.source).to include("html") - end - end - - context 'when accessing unknown stage' do - before do - visit stage_namespace_project_pipeline_path( - project.namespace, project, pipeline, format: :json, stage: 'test') - end - - it { expect(page).to have_http_status(:not_found) } - end - end - describe 'POST /:project/pipelines' do let(:project) { create(:project) } -- cgit v1.2.1 From ec4b1bc7556848c6683546559290a6576301c05d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 20 Dec 2016 16:06:01 +0100 Subject: Add isolated view spec for pipeline stage partial --- .../projects/pipelines/_stage.html.haml_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/views/projects/pipelines/_stage.html.haml_spec.rb diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb new file mode 100644 index 00000000000..eb7f7ca4a1a --- /dev/null +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'projects/pipelines/_stage', :view do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:stage) { build(:ci_stage, pipeline: pipeline) } + + 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 + + expect(rendered).to have_text 'test:build' + end +end -- cgit v1.2.1 From 5652da8bb4ea26acd35a241683e242439fefdd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Dec 2016 18:58:24 +0100 Subject: Allow unauthenticated access to Repositories Files API GET endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- changelogs/unreleased/4269-public-api.yml | 2 +- changelogs/unreleased/4269-public-files-api.yml | 4 +++ .../unreleased/4269-public-repositories-api.yml | 2 +- doc/api/repository_files.md | 4 ++- lib/api/files.rb | 2 -- spec/requests/api/files_spec.rb | 37 +++++++++++++++------- 6 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/4269-public-files-api.yml diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml index 560bc6a4f13..9de739d0cad 100644 --- a/changelogs/unreleased/4269-public-api.yml +++ b/changelogs/unreleased/4269-public-api.yml @@ -1,4 +1,4 @@ --- -title: Allow public access to some Project API endpoints +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 new file mode 100644 index 00000000000..e8f9e9b5ed3 --- /dev/null +++ b/changelogs/unreleased/4269-public-files-api.yml @@ -0,0 +1,4 @@ +--- +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 index 861307a022b..38984eed904 100644 --- a/changelogs/unreleased/4269-public-repositories-api.yml +++ b/changelogs/unreleased/4269-public-repositories-api.yml @@ -1,4 +1,4 @@ --- -title: Allow Repositories API GET endpoints to be requested anonymously +title: Allow unauthenticated access to Repositories API GET endpoints merge_request: 8148 author: diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index b8c9eb2c9a8..8a6baed5987 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -6,7 +6,9 @@ ## Get file from repository -Allows you to receive information about file in repository like name, size, content. Note that file content is Base64 encoded. +Allows you to receive information about file in repository like name, size, +content. Note that file content is Base64 encoded. This endpoint can be accessed +without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/files diff --git a/lib/api/files.rb b/lib/api/files.rb index 28f306e45f3..532a317c89e 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,8 +1,6 @@ module API # Projects API class Files < Grape::API - before { authenticate! } - helpers do def commit_params(attrs) { diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 2081f80ccc1..4dd312e2852 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -24,19 +24,34 @@ describe API::Files, api: true do before { project.team << [user, :developer] } describe "GET /projects/:id/repository/files" do - it "returns file info" do - params = { - file_path: file_path, - ref: 'master', - } + shared_examples_for 'repository files' do + it "returns file info" do + params = { + file_path: file_path, + ref: 'master', + } - get api("/projects/#{project.id}/repository/files", user), params + get api("/projects/#{project.id}/repository/files", current_user), params - expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end end it "returns a 400 bad request if no params given" do -- cgit v1.2.1 From 0349e83aa74b42c3f564fd1bc34104300a41ddf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 20 Dec 2016 15:05:57 +0100 Subject: Improve specs for Files API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/requests/api/files_spec.rb | 74 +++++++++++++++++-------- spec/requests/api/repositories_spec.rb | 11 ---- spec/support/api/repositories_shared_context.rb | 10 ++++ 3 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 spec/support/api/repositories_shared_context.rb diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 4dd312e2852..685da28c673 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -4,7 +4,14 @@ describe API::Files, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let(:file_path) { 'files/ruby/popen.rb' } + let(:params) do + { + file_path: file_path, + ref: 'master' + } + end let(:author_email) { FFaker::Internet.email } # I have to remove periods from the end of the name @@ -24,14 +31,11 @@ describe API::Files, api: true do before { project.team << [user, :developer] } describe "GET /projects/:id/repository/files" do + let(:route) { "/projects/#{project.id}/repository/files" } + shared_examples_for 'repository files' do it "returns file info" do - params = { - file_path: file_path, - ref: 'master', - } - - get api("/projects/#{project.id}/repository/files", current_user), params + get api(route, current_user), params expect(response).to have_http_status(200) expect(json_response['file_path']).to eq(file_path) @@ -39,36 +43,60 @@ describe API::Files, api: true do expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end + + context 'when no params are given' do + it_behaves_like '400 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'when file_path does not exist' do + let(:params) do + { + file_path: 'app/models/application.rb', + ref: 'master', + } + end + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user), params } + let(:message) { '404 File Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user), params } + end + end end - context 'when unauthenticated' do + context 'when unauthenticated', 'and project is public' do it_behaves_like 'repository files' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end - context 'when authenticated' do - it_behaves_like 'repository files' do - let(:current_user) { user } + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route), params } + let(:message) { '404 Project Not Found' } end end - it "returns a 400 bad request if no params given" do - get api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_http_status(400) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end end - it "returns a 404 if such file does not exist" do - params = { - file_path: 'app/models/application.rb', - ref: 'master', - } - - get api("/projects/#{project.id}/repository/files", user), params - - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest), params } + end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index fe28ad1d1a1..0b19fa38c55 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -11,17 +11,6 @@ describe API::Repositories, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:master) { create(:project_member, :master, user: user, project: project) } - shared_context 'disabled repository' do - before do - project.project_feature.update_attributes!( - repository_access_level: ProjectFeature::DISABLED, - merge_requests_access_level: ProjectFeature::DISABLED, - builds_access_level: ProjectFeature::DISABLED - ) - expect(project.feature_available?(:repository, current_user)).to be false - end - end - describe "GET /projects/:id/repository/tree" do let(:route) { "/projects/#{project.id}/repository/tree" } diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb new file mode 100644 index 00000000000..ea38fe4f5b8 --- /dev/null +++ b/spec/support/api/repositories_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'disabled repository' do + before do + project.project_feature.update_attributes!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + expect(project.feature_available?(:repository, current_user)).to be false + end +end -- cgit v1.2.1 From 33564b113386b3b9c1606dcc2ff7251c178667e5 Mon Sep 17 00:00:00 2001 From: jurre Date: Tue, 20 Dec 2016 15:23:20 +0100 Subject: Make 'unmarked as WIP' message more consistent --- app/services/system_note_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8b48d90f60b..7613ecd5021 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -146,7 +146,7 @@ module SystemNoteService end def remove_merge_request_wip(noteable, project, author) - body = 'unmarked as a Work In Progress' + body = 'unmarked as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) end -- cgit v1.2.1 From ca979f2a14fbddf538cd933acefff05d2b049633 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 20 Dec 2016 16:50:39 +0100 Subject: Use gitlab-workhorse 1.2.1 --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 26aaba0e866..6085e946503 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.2.0 +1.2.1 -- cgit v1.2.1 From b82fdf6257255b720526ccef716759892e88de09 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 20 Dec 2016 17:52:27 +0100 Subject: 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 +++- .../unreleased/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 -- cgit v1.2.1 From 9c623e3e5d7434f2e30f7c389d13e5af4ede770a Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 20 Dec 2016 14:48:04 +0000 Subject: Added QueryRecorder to test N+1 fix on Milestone#show --- spec/features/milestones/show_spec.rb | 26 +++++++++++++++++++++++ spec/support/query_recorder.rb | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/features/milestones/show_spec.rb create mode 100644 spec/support/query_recorder.rb diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb new file mode 100644 index 00000000000..40b4dc63697 --- /dev/null +++ b/spec/features/milestones/show_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe 'Milestone show', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:milestone) { create(:milestone, project: project) } + let(:labels) { create_list(:label, 2, project: project) } + let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } } + + before do + project.add_user(user, :developer) + login_as(user) + end + + def visit_milestone + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'avoids N+1 database queries' do + create(:labeled_issue, issue_params) + control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + create_list(:labeled_issue, 10, issue_params) + + expect { visit_milestone }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb new file mode 100644 index 00000000000..e40d5ebd9a8 --- /dev/null +++ b/spec/support/query_recorder.rb @@ -0,0 +1,40 @@ +module ActiveRecord + class QueryRecorder + attr_reader :log + + def initialize(&block) + @log = [] + ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) + end + + def callback(name, start, finish, message_id, values) + return if %w(CACHE SCHEMA).include?(values[:name]) + @log << values[:sql] + end + + def count + @log.count + end + + def log_message + @log.join("\n\n") + end + end +end + +RSpec::Matchers.define :exceed_query_limit do |expected| + supports_block_expectations + + match do |block| + query_count(&block) > expected + end + + failure_message_when_negated do |actual| + "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + end + + def query_count(&block) + @recorder = ActiveRecord::QueryRecorder.new(&block) + @recorder.count + end +end -- cgit v1.2.1 From 08bc8ad9874d646c0d7eb7dca1951b78f7e5df70 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Tue, 20 Dec 2016 17:20:41 +0000 Subject: 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' -- cgit v1.2.1 From 8742c109503c4ad56c99481e22d9fe90e504a371 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Tue, 20 Dec 2016 17:38:06 +0000 Subject: 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: -- cgit v1.2.1 From 61d09a7b15ef9ae2e23359f1afb87b0adbda4dd4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 19:11:53 +0100 Subject: WIP --- app/controllers/projects/mattermosts_controller.rb | 21 +++++++++++---------- 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, 40 insertions(+), 49 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) - - 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) + result, message = @service.configure(current_user, configure_params) + + 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 - - it 'gets the teams' do - expect(session).to receive(:post) + before do + Mattermost::Session.base_uri("http://mattermost.example.com") + end - 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 -- cgit v1.2.1 From 2b7d759afdb3ce0327367cb7e719afb288334d39 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 20:01:48 +0100 Subject: Add with_lease to session [ci skip] --- lib/mattermost/session.rb | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 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 - - begin - yield self - rescue Errno::ECONNREFUSED - raise NoSessionError - ensure - destroy + with_lease do + raise NoSessionError unless create + + 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 -- cgit v1.2.1 From dec1e90e505d9ab9e8b088b6a348f5bec293fed1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 20:07:05 +0100 Subject: 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 -- cgit v1.2.1 From 2b486c2bb27087e4eb306821b9fca95ff8ac74d3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 20 Dec 2016 20:07:34 +0100 Subject: Fix stage and pipeline specs and rubocop offenses --- .../projects/pipelines_controller_spec.rb | 19 ++++++++-------- spec/features/projects/pipelines/pipelines_spec.rb | 25 +++++++++++++++------- spec/models/ci/pipeline_spec.rb | 6 +++--- spec/models/ci/stage_spec.rb | 2 +- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 94113250c9f..5fe7e6407cc 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -12,14 +12,6 @@ describe Projects::PipelinesController do end describe 'GET stages.json' do - def get_stage(name) - get :stage, namespace_id: project.namespace.path, - project_id: project.path, - id: pipeline.id, - stage: name, - format: :json - end - context 'when accessing existing stage' do before do create(:ci_build, pipeline: pipeline, stage: 'build') @@ -39,8 +31,17 @@ describe Projects::PipelinesController do get_stage('test') end - it { expect(response).to have_http_status(:not_found) } + it 'responds with not found' do + expect(response).to have_http_status(:not_found) + end end + def get_stage(name) + get :stage, namespace_id: project.namespace.path, + project_id: project.path, + id: pipeline.id, + stage: name, + format: :json + end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index fa8ba21b389..1ff57f92c4c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Pipelines", feature: true, js:true do +describe 'Pipelines', :feature, :js do include GitlabRoutingHelper include WaitForAjax @@ -70,23 +70,32 @@ describe "Pipelines", feature: true, js:true do end context 'with manual actions' do - let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + let!(:manual) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'manual build', + stage: 'test', + commands: 'test') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + visit namespace_project_pipelines_path(project.namespace, project) + end - it do + it 'has link to the manual action' do find('.js-pipeline-dropdown-manual-actions').click - expect(page).to have_link('Manual build') - end - context 'when playing' do + expect(page).to have_link('Manual build') + end + context 'when manual action was played' do before do find('.js-pipeline-dropdown-manual-actions').click click_link('Manual build') end - it { expect(manual.reload).to be_pending } + it 'enqueues manual action job' do + expect(manual.reload).to be_pending + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5e1a9fa8dd8..dc377d15f15 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -183,9 +183,9 @@ describe Ci::Pipeline, models: true do create(:commit_status, pipeline: pipeline, stage: 'test') end - it { expect(subject).to be_a(Ci::Stage) } - it { expect(subject.name).to eq('stage') } - it { expect(subject.statues).not_to be_empty } + it { expect(subject).to be_a Ci::Stage } + it { expect(subject.name).to eq 'test' } + it { expect(subject.statuses).not_to be_empty } end context 'without status in stage' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 786091a577d..742bedb37e4 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -30,7 +30,7 @@ describe Ci::Stage, models: true do describe '#statuses_count' do before do - create_job(:ci_build) } + create_job(:ci_build) create_job(:ci_build, stage: 'other stage') end -- cgit v1.2.1 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: Fix rubocop errors [ci skip] --- app/models/project_services/mattermost_slash_commands_service.rb | 6 +++--- lib/mattermost/client.rb | 2 +- spec/features/projects/services/mattermost_slash_command_spec.rb | 1 - .../project_services/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 -- cgit v1.2.1 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: 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 -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From fed29117de6f30055d88daaa497ec18f85397ad6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 20 Dec 2016 19:14:33 -0200 Subject: Rename SlackNotificationService back to SlackService --- app/models/project.rb | 2 +- .../project_services/slack_notification_service.rb | 40 --------------------- app/models/project_services/slack_service.rb | 40 +++++++++++++++++++++ app/models/service.rb | 2 +- ...20141006143943_move_slack_service_to_webhook.rb | 2 +- ..._slack_service_to_slack_notification_service.rb | 13 +++---- lib/api/services.rb | 2 +- .../import_export/test_project_export.tar.gz | Bin 679415 -> 682154 bytes .../projects/services/slack_service_spec.rb | 4 +-- spec/lib/gitlab/import_export/all_models.yml | 2 +- .../slack_notification_service_spec.rb | 5 --- spec/models/project_services/slack_service_spec.rb | 5 +++ spec/models/project_spec.rb | 2 +- 13 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 app/models/project_services/slack_notification_service.rb create mode 100644 app/models/project_services/slack_service.rb delete mode 100644 spec/models/project_services/slack_notification_service_spec.rb create mode 100644 spec/models/project_services/slack_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index 72d3da64f2d..5807ea5acdc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -96,7 +96,7 @@ class Project < ActiveRecord::Base has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_notification_service, dependent: :destroy has_one :slack_slash_commands_service, dependent: :destroy - has_one :slack_notification_service, dependent: :destroy + has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb deleted file mode 100644 index 3cbf89efba4..00000000000 --- a/app/models/project_services/slack_notification_service.rb +++ /dev/null @@ -1,40 +0,0 @@ -class SlackNotificationService < ChatNotificationService - def title - 'Slack notifications' - end - - def description - 'Receive event notifications in Slack' - end - - def to_param - 'slack_notification' - end - - def help - 'This service sends notifications about projects events to Slack channels.
- To setup this service: -
    -
  1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  2. -
  3. Paste the Webhook URL into the field below.
  4. -
  5. Select events below to enable notifications. The channel and username are optional.
  6. -
' - end - - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - - def default_channel - "#general" - end -end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb new file mode 100644 index 00000000000..0583470d3b5 --- /dev/null +++ b/app/models/project_services/slack_service.rb @@ -0,0 +1,40 @@ +class SlackService < ChatNotificationService + def title + 'Slack notifications' + end + + def description + 'Receive event notifications in Slack' + end + + def to_param + 'slack' + end + + def help + 'This service sends notifications about projects events to Slack channels.
+ To setup this service: +
    +
  1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  2. +
  3. Paste the Webhook URL into the field below.
  4. +
  5. Select events below to enable notifications. The channel and username are optional.
  6. +
' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#general" + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 8abd8e73e43..918ed8206e0 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -222,7 +222,7 @@ class Service < ActiveRecord::Base pushover redmine slack_slash_commands - slack_notification + slack teamcity ] end diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 42e88d6d6e3..561184615cc 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -5,7 +5,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"' def change - SlackNotificationService.all.each do |slack_service| + SlackService.all.each do |slack_service| if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } token = slack_service.properties['token'] subdomain = slack_service.properties['subdomain'] diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb index a7278d7b5a6..dc38d0ac906 100644 --- a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb +++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb @@ -1,14 +1,11 @@ class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - DOWNTIME = true - DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService' + DOWNTIME = false - def up - execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'") - end - - def down - execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'") + # This migration is a no-op, as it existed in an RC but we renamed + # SlackNotificationService back to SlackService: + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8191#note_20310845 + def change end end diff --git a/lib/api/services.rb b/lib/api/services.rb index aa97f6af0b2..44e668e3cf5 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -480,7 +480,7 @@ module API desc: 'The description of the tracker' } ], - 'slack-notification' => [ + 'slack' => [ { required: true, name: :webhook, diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index d3165d07d7b..7655c2b351f 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 320ed13a01d..16541f51d98 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' feature 'Projects > Slack service > Setup events', feature: true do let(:user) { create(:user) } - let(:service) { SlackNotificationService.new } - let(:project) { create(:project, slack_notification_service: service) } + let(:service) { SlackService.new } + let(:project) { create(:project, slack_service: service) } background do service.fields diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7e618e2fcf5..155cf8565b5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -137,7 +137,7 @@ project: - assembla_service - asana_service - gemnasium_service -- slack_notification_service +- slack_service - mattermost_notification_service - buildkite_service - bamboo_service diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb deleted file mode 100644 index 110b5bf2115..00000000000 --- a/spec/models/project_services/slack_notification_service_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe SlackNotificationService, models: true do - it_behaves_like "slack or mattermost notifications" -end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb new file mode 100644 index 00000000000..9a3ecc66d83 --- /dev/null +++ b/spec/models/project_services/slack_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe SlackService, models: true do + it_behaves_like "slack or mattermost notifications" +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0455cd2fe49..569071c0418 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -21,7 +21,7 @@ describe Project, models: true do it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } - it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } -- cgit v1.2.1 From d1bf557aacb5dd789ccc88786b47ec174ed1de2b Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 20 Dec 2016 19:29:39 -0200 Subject: Rename MattermostNotificationService back to MattermostService --- app/models/project.rb | 2 +- .../mattermost_notification_service.rb | 41 ---------------------- app/models/project_services/mattermost_service.rb | 41 ++++++++++++++++++++++ app/models/service.rb | 2 +- lib/api/services.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 2 +- .../mattermost_notification_service_spec.rb | 5 --- .../project_services/mattermost_service_spec.rb | 5 +++ spec/models/project_spec.rb | 2 +- 9 files changed, 51 insertions(+), 51 deletions(-) delete mode 100644 app/models/project_services/mattermost_notification_service.rb create mode 100644 app/models/project_services/mattermost_service.rb delete mode 100644 spec/models/project_services/mattermost_notification_service_spec.rb create mode 100644 spec/models/project_services/mattermost_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index 5807ea5acdc..26fa20f856d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -94,7 +94,7 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy - has_one :mattermost_notification_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy has_one :slack_slash_commands_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb deleted file mode 100644 index de18c4b1f00..00000000000 --- a/app/models/project_services/mattermost_notification_service.rb +++ /dev/null @@ -1,41 +0,0 @@ -class MattermostNotificationService < ChatNotificationService - def title - 'Mattermost notifications' - end - - def description - 'Receive event notifications in Mattermost' - end - - def to_param - 'mattermost_notification' - end - - def help - 'This service sends notifications about projects events to Mattermost channels.
- To set up this service: -
    -
  1. Enable incoming webhooks in your Mattermost installation.
  2. -
  3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  4. -
  5. Paste the webhook URL into the field bellow.
  6. -
  7. Select events below to enable notifications. The channel and username are optional.
  8. -
' - end - - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - - def default_channel - "#town-square" - end -end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 00000000000..0650f930402 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,41 @@ +class MattermostService < ChatNotificationService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.
+ To set up this service: +
    +
  1. Enable incoming webhooks in your Mattermost installation.
  2. +
  3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  4. +
  5. Paste the webhook URL into the field bellow.
  6. +
  7. Select events below to enable notifications. The channel and username are optional.
  8. +
' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#town-square" + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 918ed8206e0..19ef3ba9c23 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -216,7 +216,7 @@ class Service < ActiveRecord::Base jira kubernetes mattermost_slash_commands - mattermost_notification + mattermost pipelines_email pivotaltracker pushover diff --git a/lib/api/services.rb b/lib/api/services.rb index 44e668e3cf5..d11cdce4e18 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -500,7 +500,7 @@ module API desc: 'The channel name' } ], - 'mattermost-notification' => [ + 'mattermost' => [ { required: true, name: :webhook, diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 155cf8565b5..f420d71dee2 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -138,7 +138,7 @@ project: - asana_service - gemnasium_service - slack_service -- mattermost_notification_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb deleted file mode 100644 index 7832d6f50cf..00000000000 --- a/spec/models/project_services/mattermost_notification_service_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe MattermostNotificationService, models: true do - it_behaves_like "slack or mattermost notifications" -end diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb new file mode 100644 index 00000000000..490d6aedffc --- /dev/null +++ b/spec/models/project_services/mattermost_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostService, models: true do + it_behaves_like "slack or mattermost notifications" +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 569071c0418..88d5d14f855 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -22,7 +22,7 @@ describe Project, models: true do it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } - it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } -- cgit v1.2.1 From 70dcd45de207992b77b23175d7ed9e4643342f72 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 23:08:55 +0100 Subject: 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?' -- cgit v1.2.1 From 22a0567823e792bd760ced79539a0b2c4bfd8f5e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 20 Dec 2016 23:17:32 +0100 Subject: 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) -- cgit v1.2.1 From 85666b7077728aeb2015030d603bc9c9fe6686c6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 20 Dec 2016 17:21:02 -0600 Subject: 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 */ -- cgit v1.2.1 From e9587d5038cbaeca718d6dc08acf906780c0eed0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 20 Dec 2016 17:21:02 -0600 Subject: 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 */ -- cgit v1.2.1 From 35adeee5148eddebc003abcd690ad01a50219bde Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 12 Dec 2016 17:24:49 -0600 Subject: homogenize revert and cherry-pick button styles generated by commits_helper --- app/helpers/commits_helper.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 66a720a9426..4d61833d624 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -131,9 +131,9 @@ module CommitsHelper return unless current_user tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? if can_collaborate_with_project? - btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { @@ -145,8 +145,6 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - btn_class = "btn btn-grouped btn-warning" unless btn_class.nil? - link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end @@ -154,10 +152,10 @@ module CommitsHelper def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" + tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? if can_collaborate_with_project? - btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { @@ -169,8 +167,7 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - btn_class = "btn btn-grouped btn-close" unless btn_class.nil? - link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) + link_to 'Cherry-pick', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end -- cgit v1.2.1 From 26a80149b0381e678ac6b4f13340e81055be3406 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 13 Dec 2016 09:49:05 -0600 Subject: factor out common code to satisfy rake flay --- app/helpers/commits_helper.rb | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 4d61833d624..e7d78cc422e 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -136,15 +136,7 @@ module CommitsHelper if can_collaborate_with_project? link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to revert this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - + fork_path = fork_path_url(continue_to_path, message: 'Try to revert this commit again.') link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end @@ -158,15 +150,7 @@ module CommitsHelper if can_collaborate_with_project? link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - + fork_path = fork_path_url(continue_to_path, message: 'Try to cherry-pick this commit again.') link_to 'Cherry-pick', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end @@ -208,6 +192,20 @@ module CommitsHelper end end + def fork_path_url(continue_to_path, message: nil) + notice = edit_in_new_fork_notice + notice << " #{message}" unless message.nil? + + continue_params = { + to: continue_to_path, + notice: notice, + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + end + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, -- cgit v1.2.1 From 4e3b9e03f8ef64a39f587c53e45b2f8d4cf571db Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 13 Dec 2016 09:57:01 -0600 Subject: remove button class size alteration from revert and cherry pick links --- app/helpers/commits_helper.rb | 4 ++-- app/views/projects/merge_requests/widget/_merged_buttons.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e7d78cc422e..c7d1de0e3b7 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -131,7 +131,7 @@ module CommitsHelper return unless current_user tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? if can_collaborate_with_project? link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" @@ -145,7 +145,7 @@ module CommitsHelper return unless current_user tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? if can_collaborate_with_project? link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index d836a253507..81abb543d57 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -9,6 +9,6 @@ = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") - if mr_can_be_cherry_picked - = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") -- cgit v1.2.1 From 759e37f3b192d791e32bda795f4731b29c17d552 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 14 Dec 2016 00:30:38 -0600 Subject: reduce common code even further to satisfy rake flay --- app/helpers/commits_helper.rb | 48 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index c7d1de0e3b7..f1897119e20 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,31 +128,11 @@ module CommitsHelper end def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - btn_class = "btn btn-#{btn_class}" unless btn_class.nil? - - if can_collaborate_with_project? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - fork_path = fork_path_url(continue_to_path, message: 'Try to revert this commit again.') - link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - btn_class = "btn btn-#{btn_class}" unless btn_class.nil? - - if can_collaborate_with_project? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - fork_path = fork_path_url(continue_to_path, message: 'Try to cherry-pick this commit again.') - link_to 'Cherry-pick', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end protected @@ -192,6 +172,28 @@ module CommitsHelper end end + def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true) + return unless current_user + + tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? + + if can_collaborate_with_project? + link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) + end + end + def fork_path_url(continue_to_path, message: nil) notice = edit_in_new_fork_notice notice << " #{message}" unless message.nil? @@ -201,7 +203,7 @@ module CommitsHelper notice: notice, notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, continue: continue_params) end -- cgit v1.2.1 From c9165f845974134917927aa174a214a334d264fe Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 19 Dec 2016 11:12:30 -0600 Subject: remove unused helper method --- app/helpers/commits_helper.rb | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index f1897119e20..e9461b9f859 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -194,20 +194,6 @@ module CommitsHelper end end - def fork_path_url(continue_to_path, message: nil) - notice = edit_in_new_fork_notice - notice << " #{message}" unless message.nil? - - continue_params = { - to: continue_to_path, - notice: notice, - notice_now: edit_in_new_fork_notice_now - } - namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - end - def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, -- cgit v1.2.1 From eda7db3bfbf915ccbfebe045807dafcc25b0a6a1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 19 Dec 2016 13:00:06 -0500 Subject: =?UTF-8?q?Set=20=E2=80=9CRemove=20branch=E2=80=9D=20button=20to?= =?UTF-8?q?=20default=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/projects/merge_requests/widget/_merged_buttons.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 81abb543d57..9eef011b591 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -5,7 +5,7 @@ - if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .clearfix.merged-buttons - if can_remove_source_branch - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted -- cgit v1.2.1 From 071745141d86385aaa4215be1a5d829618962bb0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 2 Dec 2016 16:44:49 +0530 Subject: 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 -- cgit v1.2.1 From f1f45d4a6f8942040d9741604f503538e991f70a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:02:45 +0530 Subject: 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 | 130 +++++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 47 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(); - - 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 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) { @@ -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() { -- cgit v1.2.1 From 54fe47b876ebb717616140fb7aa0e506823f6dd1 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:03:28 +0530 Subject: Enhance styling of scroll buttons, make build log wide --- app/assets/stylesheets/pages/builds.scss | 68 +++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 10 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; + z-index: 1; } - &.affix { - right: 25px; - bottom: 15px; - z-index: 1; + .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 { - right: #{$gutter_width + ($gl-padding * 2)}; + + .btn.sticky, + .autoscroll-container { + right: #{$gutter_width + ($gl-padding * 2)}; + } } - a { - display: block; - margin-bottom: 10px; + @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}); -- cgit v1.2.1 From c4dfd07b3615b008feeb177f0aa61d0f83b9f696 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:03:54 +0530 Subject: 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") -- cgit v1.2.1 From 394d0fd3c8d07538af9d2065178ee4845b25c7d3 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:23:51 +0530 Subject: 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'); } }; -- cgit v1.2.1 From 3ea8d17e8756fa2fc7978a6ea815ff37ded2e76b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:40:01 +0530 Subject: 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'); } }; -- cgit v1.2.1 From 769a9bcc48d1969f65988f0caae26cd2129e8e3c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 18:03:20 +0530 Subject: 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 -- cgit v1.2.1 From 3d134c50b3baf83b5f675d5e6bda2171924d2ab8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 16:02:45 +0530 Subject: 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() { -- cgit v1.2.1 From daa08cbe04028e8303321e16dd96c71bb7a6c703 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:23:51 +0530 Subject: 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 }; -- cgit v1.2.1 From cf669e5d5a61465aa605b5d31253fe23a5569b00 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 17:40:01 +0530 Subject: 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 }; -- cgit v1.2.1 From 7f6ac4f44c8cf071a6ec2f3f1ffc400dcb5d1e40 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Dec 2016 20:00:34 +0530 Subject: 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 -- cgit v1.2.1 From 985e84a3b24f18f8dd3e15d2f7225124f0c2d5c6 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 15:47:05 +0100 Subject: 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; } -- cgit v1.2.1 From 65082b66c3f3baaa088d0ee7bc5b59661960c020 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 15:56:39 +0100 Subject: 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; -- cgit v1.2.1 From 407c90bebf09fb0668940ba93ddec875a4644116 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 5 Dec 2016 16:01:06 +0100 Subject: 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 { -- cgit v1.2.1 From 8d7d6bcbd02020ea4b75cb4cec31dfd4b2b6292b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 15:04:42 +0530 Subject: 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() { -- cgit v1.2.1 From 1f22e7dc8581378bc75dd01822869c2d08650624 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:24:18 +0530 Subject: 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 @@ + -- cgit v1.2.1 From 16ad6ed0387f0152ed3a71e643b948be59bcf3d7 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:25:36 +0530 Subject: 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 -- cgit v1.2.1 From ba63e4f1ed9f8d8e452a80c6f96b50c5487510c8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 6 Dec 2016 16:27:08 +0530 Subject: 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; } - } - } - #scroll-top.btn.sticky { - top: 110px; - } - - #scroll-bottom.btn.sticky { - bottom: -2px; + &:hover label { + opacity: 1; + } + } } &.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}); -- cgit v1.2.1 From c245a667d279066ee819230ec373ee0502c0e0f4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:07:30 +0530 Subject: 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 { -- cgit v1.2.1 From 1549db64715596437332aaee64eef3f2655a1c77 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:07:59 +0530 Subject: 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'); } } -- cgit v1.2.1 From 46f0633272e5a9af708bbb09f6c6df9b7734ba0c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:32:43 +0530 Subject: 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 -- cgit v1.2.1 From f06678cb1af25a4d7e3b3dae29c15eda58935e59 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 8 Dec 2016 16:33:00 +0530 Subject: 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(); } } -- cgit v1.2.1 From ff0c3fc23b9808438c8959e835937c48297de9b3 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 8 Dec 2016 14:08:08 +0100 Subject: 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 -- cgit v1.2.1 From 2421fc7e089d634e0069285cf2addb0745df53dd Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:06 +0530 Subject: Refactor SCSS: fix SCSSLints, simplify specificity --- app/assets/stylesheets/pages/builds.scss | 219 ++++++++++++++++--------------- 1 file changed, 111 insertions(+), 108 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,109 +45,109 @@ } } - .scroll-controls { - height: 100%; + .environment-information { + background-color: $gray-light; + border: 1px solid $border-color; + padding: 12px $gl-padding; + border-radius: $border-radius-default; - .scroll-step { - width: 31px; - margin: 0 0 0 auto; + svg { + position: relative; + top: 1px; + margin-right: 5px; } + } +} - .scroll-link.sticky, - .autoscroll-container { - right: 25px; - z-index: 1; - } +.scroll-controls { + height: 100%; - .scroll-link { - display: block; - margin-bottom: 10px; - width: 16px; - height: 33px; + .scroll-step { + width: 31px; + margin: 0 0 0 auto; + } - &.scroll-top { - background-image: image-url('scroll_up'); + .scroll-link.sticky, + .autoscroll-container { + right: 25px; + z-index: 1; + } - &:hover { - background-image: image-url('scroll_up_hover_active'); - } - } + .scroll-link { + display: block; + margin-bottom: 10px; + width: 16px; + height: 33px; - &.scroll-bottom { - background-image: image-url('scroll_down'); + &.scroll-top { + background-image: image-url('scroll_up'); - &:hover { - background-image: image-url('scroll_down_hover_active'); - } + &:hover { + background-image: image-url('scroll_up_hover_active'); } + } - &.sticky { - position: fixed; - - &.scroll-top { - top: 110px; - } + &.scroll-bottom { + background-image: image-url('scroll_down'); - &.scroll-bottom { - bottom: -2px; - } + &:hover { + background-image: image-url('scroll_down_hover_active'); } } - .autoscroll-container { - position: absolute; - - .status-message { - display: inline-block; - color: $white-light; + &.sticky { + position: fixed; - 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; - } - } + &.scroll-top { + top: 110px; + } - &:hover label { - opacity: 1; - } + &.scroll-bottom { + bottom: -2px; } } + } - &.sidebar-expanded { + .autoscroll-container { + position: absolute; + } - .scroll-link.sticky, - .autoscroll-container { - right: #{$gutter_width + ($gl-padding * 2)}; - } + &.sidebar-expanded { + + .scroll-link.sticky, + .autoscroll-container { + right: ($gutter_width + ($gl-padding * 2)); } } +} - .environment-information { - background-color: $gray-light; - border: 1px solid $border-color; - padding: 12px $gl-padding; - border-radius: $border-radius-default; +.status-message { + display: inline-block; + color: $white-light; - svg { - position: relative; - top: 1px; - margin-right: 5px; + .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 { @@ -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; } } -- cgit v1.2.1 From 94fd6cf74b44404e5f450a195e252b1b76e844cc Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:41 +0530 Subject: 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'); } } -- cgit v1.2.1 From a8fa87ce9d0dea1baed7c40e10a6b19da99ec6e4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:09:57 +0530 Subject: 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 -- cgit v1.2.1 From c02945ac95ee3374007249f07211cbf6002a2fa2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 17:56:25 +0530 Subject: 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'); } }; -- cgit v1.2.1 From 3202c9c01b6c638ed9a8b7195b83b862b97ee33d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:05:35 +0530 Subject: 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 @@ + + + -- cgit v1.2.1 From d144160f9f1f7b59ea2cc49d511b6a195b8b6e2d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:06:21 +0530 Subject: 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 { -- cgit v1.2.1 From 5fc161c2a78710bb026e73bd502e4955022ec3b0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 19:06:38 +0530 Subject: 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 -- cgit v1.2.1 From df0aeae7da14fa3627c5627c11c521d91170aca9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:05:36 +0530 Subject: 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'); } }; -- cgit v1.2.1 From 719e59c71066748f10bedab35b4006e37e3d025f Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:06:03 +0530 Subject: 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)); } -- cgit v1.2.1 From 6fe5bb40b6dde14b3594a37b008265e1f9083296 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:06:18 +0530 Subject: 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]; }; -- cgit v1.2.1 From ad0b81872a52e984f240f229c3de03bae4d44c7e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 9 Dec 2016 20:34:19 +0530 Subject: 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(); } -- cgit v1.2.1 From 95d535921214740d0f26f65b1567aa03b9e53b2b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 12:17:20 +0530 Subject: 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) }); }; -- cgit v1.2.1 From 2b6b28da740476c02a7bab2113737458e3e6af30 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 12:17:50 +0530 Subject: 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); -- cgit v1.2.1 From 45220e310cd12da9bd7b0b07fe3509def063a60c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 22:05:17 +0530 Subject: 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) }); }; -- cgit v1.2.1 From 3ed1f6dca770f11e9b86eb3163bbd5476c94e4e0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 12 Dec 2016 22:12:24 +0530 Subject: 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") { -- cgit v1.2.1 From dfd156972df7d47465e9222e1979fa9c91b702aa Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 18:14:04 +0530 Subject: 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 - this.$scrollTopBtn.hide(); + 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 (gl.utils.isInViewport(this.$downBuildTrace[0])) { // User is at Bottom of Build Log + // 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(); + } 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'); } }; -- cgit v1.2.1 From cc62a416560267d4a87b823faa333bcdaa5867b4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 21:13:27 +0530 Subject: 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(); -- cgit v1.2.1 From 3f2fd48c7cdcbefc4d26b5b45c9150433d394271 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 13 Dec 2016 21:21:13 +0530 Subject: 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(); -- cgit v1.2.1 From 63ca9fd0c85fbb5364ee8bf96e78e8ea6bb0427b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 14 Dec 2016 21:46:45 +0530 Subject: 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 { - top: 110px; - - .gitlab-icon-scroll-up-hover { - display: none; - } - - &:hover { + &.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; + } - .gitlab-icon-scroll-up { - display: none; - } + &.scroll-top:hover .gitlab-icon-scroll-up-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down-hover { + display: inline-block; + } - .gitlab-icon-scroll-up-hover { - display: inline-block; - } - } + &.scroll-top { + top: 110px; } &.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; } -- cgit v1.2.1 From 3817daa37a77193c5f9479136a06c4737b037818 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 14 Dec 2016 21:47:08 +0530 Subject: 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(); -- cgit v1.2.1 From dbd21446e5a9550e80ec8f3f3448e75808208e80 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Dec 2016 11:31:09 +0530 Subject: 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: -- cgit v1.2.1 From 09738ad09843721d0b0849702daf51292a372df1 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 10:04:12 +0530 Subject: 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'); } }; -- cgit v1.2.1 From 5bab7c57e3e108d603ceb41c53091ebfb027a37e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 10:11:05 +0530 Subject: 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(); -- cgit v1.2.1 From 7dfd5aa69ed0db0157b475973ffcea592098267a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:10:35 +0530 Subject: 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 -- cgit v1.2.1 From b4aabafaa48043a8b17b3bf927a684174ad31abd Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:31:51 +0530 Subject: 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(); -- cgit v1.2.1 From 11ba7a885674dd5428b92c2e43a109fa098320f4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:42:11 +0530 Subject: 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(); -- cgit v1.2.1 From 120eeb5b5163f91220572de0d50c8359f115b7a8 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 21 Dec 2016 13:58:32 +0530 Subject: Renamed with `.svg` --- app/views/shared/icons/_scroll_down.svg | 3 +++ app/views/shared/icons/_scroll_down.svg.erb | 3 --- app/views/shared/icons/_scroll_down_hover_active.svg | 3 +++ app/views/shared/icons/_scroll_down_hover_active.svg.erb | 3 --- app/views/shared/icons/_scroll_up.svg | 3 +++ app/views/shared/icons/_scroll_up.svg.erb | 3 --- app/views/shared/icons/_scroll_up_hover_active.svg | 3 +++ app/views/shared/icons/_scroll_up_hover_active.svg.erb | 3 --- 8 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 app/views/shared/icons/_scroll_down.svg delete mode 100644 app/views/shared/icons/_scroll_down.svg.erb create mode 100644 app/views/shared/icons/_scroll_down_hover_active.svg delete mode 100644 app/views/shared/icons/_scroll_down_hover_active.svg.erb create mode 100644 app/views/shared/icons/_scroll_up.svg delete mode 100644 app/views/shared/icons/_scroll_up.svg.erb create mode 100644 app/views/shared/icons/_scroll_up_hover_active.svg delete mode 100644 app/views/shared/icons/_scroll_up_hover_active.svg.erb diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg new file mode 100644 index 00000000000..acf22ac9314 --- /dev/null +++ b/app/views/shared/icons/_scroll_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/icons/_scroll_down.svg.erb b/app/views/shared/icons/_scroll_down.svg.erb deleted file mode 100644 index acf22ac9314..00000000000 --- a/app/views/shared/icons/_scroll_down.svg.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg new file mode 100644 index 00000000000..262576acf54 --- /dev/null +++ b/app/views/shared/icons/_scroll_down_hover_active.svg @@ -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 deleted file mode 100644 index 262576acf54..00000000000 --- a/app/views/shared/icons/_scroll_down_hover_active.svg.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg new file mode 100644 index 00000000000..f11288fd59c --- /dev/null +++ b/app/views/shared/icons/_scroll_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/icons/_scroll_up.svg.erb b/app/views/shared/icons/_scroll_up.svg.erb deleted file mode 100644 index f11288fd59c..00000000000 --- a/app/views/shared/icons/_scroll_up.svg.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg new file mode 100644 index 00000000000..4658dbb1bb7 --- /dev/null +++ b/app/views/shared/icons/_scroll_up_hover_active.svg @@ -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 deleted file mode 100644 index 4658dbb1bb7..00000000000 --- a/app/views/shared/icons/_scroll_up_hover_active.svg.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - -- cgit v1.2.1 From 6e186b76bb22c5803e3d7105f8ff78c11128812d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 11 Dec 2016 14:09:37 +0200 Subject: Added support for Authentiq oauth provider --- Gemfile | 1 + Gemfile.lock | 3 + app/assets/images/auth_buttons/authentiq_64.png | Bin 0 -> 17679 bytes app/helpers/auth_helper.rb | 2 +- .../unreleased/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 00000000000..81767bbcc54 Binary files /dev/null and b/app/assets/images/auth_buttons/authentiq_64.png differ diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 92bac149313..1ee6c1d3afa 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml new file mode 100644 index 00000000000..36f8ac9c840 --- /dev/null +++ b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml @@ -0,0 +1,4 @@ +--- +title: Add Authentiq as Oauth provider +merge_request: 8038 +author: Alexandros Keramidas diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index b8b41a0d86c..2d1d48bf9da 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -368,6 +368,16 @@ production: &base # login_url: '/cas/login', # service_validate_url: '/cas/p3/serviceValidate', # logout_url: '/cas/logout'} } + # - { name: 'authentiq', + # # for client credentials (client ID and secret), go to https://www.authentiq.com/ + # app_id: 'YOUR_CLIENT_ID', + # app_secret: 'YOUR_CLIENT_SECRET', + # args: { + # scope: 'aq:name email~rs address aq:push' + # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost' + # # redirect_uri: 'YOUR_REDIRECT_URI' + # } + # } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 2fc5d0355b5..13bd501e397 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -6,7 +6,7 @@ providers. - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - Bitbucket, Facebook, Shibboleth, Crowd and Azure + Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md new file mode 100644 index 00000000000..3f39539da95 --- /dev/null +++ b/doc/administration/auth/authentiq.md @@ -0,0 +1,69 @@ +# Authentiq OmniAuth Provider + +To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq. + +Authentiq will generate a Client ID and the accompanying Client Secret for you to use. + +1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). + +2. On your GitLab server, open the configuration file: + + For omnibus installation + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` + +3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider. + +4. Add the provider configuration for Authentiq: + + For Omnibus packages: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "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 -- cgit v1.2.1 From 5452747729d30733a51a85ff1212d0724af1d1ff Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 10:33:20 +0100 Subject: 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": [ -- cgit v1.2.1 From c0e3183b734f9891fef347895bf1192e7a95221e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 10:34:44 +0100 Subject: 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: -- cgit v1.2.1 From 6c81d03f53ec61cd76b56458b645ef318663ad69 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Thu, 8 Dec 2016 01:36:44 +0600 Subject: 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 { -- cgit v1.2.1 From 12d7652bcc94442f4159f9734eb9ecb560381d08 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Thu, 8 Dec 2016 01:40:38 +0600 Subject: 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: -- cgit v1.2.1 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: 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 --- changelogs/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 -- cgit v1.2.1 From fd5062a848d63b89248c384e0171c6f1af833a49 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 11:50:31 +0100 Subject: 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 -- cgit v1.2.1 From 55c61d2e412733b0feefcb5fa33a96e584ffe2bc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 11:53:44 +0100 Subject: Improve API specs --- .../mattermost_slash_commands_service.rb | 6 +- .../unreleased/mattermost-slash-auto-config.yml | 4 + lib/mattermost/session.rb | 32 +++--- spec/lib/mattermost/command_spec.rb | 57 ++++++++-- spec/lib/mattermost/team_spec.rb | 76 +++++++++---- .../mattermost_slash_commands_service_spec.rb | 119 +++++++++++++++++---- 6 files changed, 228 insertions(+), 66 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") - end + Mattermost::Session.base_uri('http://mattermost.example.com') - subject { described_class.new(user) } + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end 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 { 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 - subject.create(params) + 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 }] - end + 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 - subject { described_class.new(user) } + 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 - before do - allow(subject).to receive(:json_get).and_return(response) + it 'returns a token' do + is_expected.to eq(response) + end end - it 'gets the teams' do - expect(subject.all.count).to be(1) + 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 + + 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 + + it 'shows error messages' do + succeeded, message = subject - expect(service.reload.token).to eq('token') + 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 + + 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(succeeded).to be(false) - expect(message).to start_with("Failed to open TCP connection to") + expect(teams).to be_empty + expect(message).to eq('Failed to get team list.') + end end end end -- cgit v1.2.1 From 8b92e9c08b730b02816f501c88e3c237fa77d813 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 13:00:10 +0200 Subject: 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: -- cgit v1.2.1 From 21153f3e9e4af7cba2128f429b5f5267d52af58c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 20 Dec 2016 18:12:19 +0200 Subject: Rename groups with .git in the end of the path Signed-off-by: Dmitriy Zaporozhets --- changelogs/unreleased/dz-rename-invalid-groups.yml | 4 ++ ...161220141214_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 -- cgit v1.2.1 From dfa842ce1de5b93bca2f336f3cf8c8769a555853 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 24 Nov 2016 15:45:34 +0000 Subject: Removes builds tab from merge request Fix specs --- app/assets/javascripts/merge_request_tabs.js | 416 +++++++++++++++++++++ app/assets/javascripts/merge_request_widget.js.es6 | 4 +- .../projects/merge_requests_controller.rb | 4 +- .../projects/merge_requests/_new_submit.html.haml | 11 +- app/views/projects/merge_requests/_show.html.haml | 7 - .../projects/merge_requests/widget/_show.html.haml | 2 - config/routes/project.rb | 1 - .../projects/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 -- cgit v1.2.1 From 9a82aa70e3f3ff679c7a27ccbb9d497576b20822 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 25 Nov 2016 18:10:32 +0000 Subject: 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 ------- .../projects/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 -- cgit v1.2.1 From 7086dac42fff6e32bc38b389ffa104d9c21a159c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 29 Nov 2016 13:02:28 +0000 Subject: 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 -- cgit v1.2.1 From bb992afef64b5e9e3e6d44767942c77b42079da2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 1 Dec 2016 11:51:44 +0000 Subject: 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: -- cgit v1.2.1 From 5463ee66c850f5a31274a06bc27669ed127df811 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 15 Dec 2016 23:26:14 +0000 Subject: Remove builds from merge request tabs file --- app/assets/javascripts/merge_request_tabs.js | 416 --------------------- app/assets/javascripts/merge_request_tabs.js.es6 | 29 +- app/assets/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); -- cgit v1.2.1 From 8db9888ffcfc98a1ceb8ea2592014522df927c4d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:18:57 +0000 Subject: 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; -- cgit v1.2.1 From 80d1ead188815282928762ddcc1bd69ed78f490f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:21:11 +0000 Subject: 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; -- cgit v1.2.1 From cfcefa154f4b94939dba9f3b56ff0ea001eb72c3 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 21 Dec 2016 11:35:26 +0000 Subject: 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); -- cgit v1.2.1 From 6023224db1b161164a0af968b7c42066c43dfaeb Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva Date: Wed, 21 Dec 2016 11:43:38 +0000 Subject: 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 -- cgit v1.2.1 From f93b876c6d0e580a9532b76855cba23f5fd5f3e7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 11:40:39 +0000 Subject: 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: -- cgit v1.2.1 From 3bbe19d39bd7f2bebb1f68de99dfb791eaa7a928 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 20 Dec 2016 17:41:09 +0100 Subject: Backport moving MR widget CI JS out of script tag. --- .../merge_request_widget/ci_bundle.js.es6 | 24 ++++++++++++++++++++++ .../merge_requests/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" -- cgit v1.2.1 From 4ec259fd36fe57aa95446c10a47d784dae2c8f00 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 21 Dec 2016 11:44:47 +0100 Subject: 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 -- cgit v1.2.1 From 4c3ec579e5660daab23d48bd4b3e21050735f726 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 13:29:27 +0100 Subject: 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 -- cgit v1.2.1 From a9f85d11a584a5a9ad99aaf6366f22fd6f6d0957 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 21 Dec 2016 11:03:52 +0000 Subject: Fix space issue and add test --- app/assets/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 -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From 804198ab7f1bdd854c64da729a04fc65f2b6ff7d Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 15 Dec 2016 10:07:41 -0500 Subject: Maintain milestone filter option when updating filter Setup teaspoon tests for Issuable --- app/assets/javascripts/issuable.js.es6 | 14 ++-- .../shared/issuable/_milestone_dropdown.html.haml | 2 +- .../javascripts/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); + }); + }); + }); +})(); -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From dc303e4714b080924615ab5e61661e75fa87570c Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Wed, 21 Dec 2016 13:56:10 +0100 Subject: 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; -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From b902784dbff64d1f746fc38249a814debb8ed325 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Dec 2016 14:21:18 +0100 Subject: 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 -- cgit v1.2.1 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: 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 ++++ db/post_migrate/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" -- cgit v1.2.1 From 5a4b3e8e302eaa263203d84e93862a89b5e90d90 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 21 Dec 2016 08:25:59 -0600 Subject: 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 -- cgit v1.2.1 From e1bf40e293409d974a8013685ec544c0f633ef16 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 16:43:27 +0200 Subject: 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: -- cgit v1.2.1 From 9809a9af8a3980f8a65262295cfd9701e793ac11 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 21 Dec 2016 16:21:55 +0100 Subject: 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 +-- .../project_services/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 00000000000..9e2cd57a0ba Binary files /dev/null and b/doc/ci/autodeploy/img/autodeploy_button.png differ diff --git a/doc/ci/autodeploy/img/autodeploy_dropdown.png b/doc/ci/autodeploy/img/autodeploy_dropdown.png new file mode 100644 index 00000000000..1486a8ec0ea Binary files /dev/null and b/doc/ci/autodeploy/img/autodeploy_dropdown.png differ 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) } -- cgit v1.2.1 From d8740cfe19ea033a119cb4530aeb41fcec4bafca Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 21 Dec 2016 17:30:29 +0100 Subject: Move javascript for widget check to ci_bundle. --- .../merge_request_widget/ci_bundle.js.es6 | 56 ++++++++++++++-------- .../merge_requests/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'); + }); + + $('.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-merge-dropdown a').on('click', (e) => { - e.preventDefault(); - $(this).closest('form').submit(); + $('.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(); - }); - -- cgit v1.2.1 From 15d83f6ae2e3b52a79e761a63c86907a6161acec Mon Sep 17 00:00:00 2001 From: Makoto Scott-Hinkle Date: Sat, 1 Oct 2016 13:53:08 -0700 Subject: Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. --- .../unreleased/22742-filter-protocol-relative-urls.yml | 4 ++++ lib/banzai/filter/external_link_filter.rb | 2 +- spec/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 -- cgit v1.2.1 From 3d0074a6edc22eb6840bad639c8f4fde6be89293 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 12:20:18 +0000 Subject: 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: -- cgit v1.2.1 From fd4e9c53ddc5c1c1ae98794937d479407511b0a3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 17:41:41 +0000 Subject: 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 { -- cgit v1.2.1 From 52f255eb8fdee19239f8cb84affb911a6e784a91 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 17:57:56 +0000 Subject: 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; -- cgit v1.2.1 From 7af884a5094b505d3b59c2d289237afd9148119c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 18:00:22 +0000 Subject: 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: -- cgit v1.2.1 From 27c613cb1818f5bca695f667feb5ba12eee19d59 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 21 Dec 2016 18:29:10 +0000 Subject: 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" -- cgit v1.2.1 From d0c5222de2c435f74ac7dfd99194afbd292f2dd2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 21 Dec 2016 18:35:16 +0000 Subject: 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: -- cgit v1.2.1 From 1c2d9015da095a9c4148cd5455e9ae4e8f854674 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 21 Dec 2016 21:08:55 +0200 Subject: 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: -- cgit v1.2.1 From 3cb8f01b9a33925bd65347f4a97c6db2f787589c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 21:13:13 +0100 Subject: 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 -- cgit v1.2.1 From 4a1e1281ac862a0c86d62473ab09d559c7ec5485 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 21 Dec 2016 21:25:23 +0100 Subject: 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 ++--- .../project_services/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) } -- cgit v1.2.1 From ef99ffd76de31b109771dc091356b470629210fd Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:20:57 +0900 Subject: Rname katex.css to katex.scss --- vendor/assets/stylesheets/katex.css | 975 ----------------------------------- vendor/assets/stylesheets/katex.scss | 975 +++++++++++++++++++++++++++++++++++ 2 files changed, 975 insertions(+), 975 deletions(-) delete mode 100644 vendor/assets/stylesheets/katex.css create mode 100644 vendor/assets/stylesheets/katex.scss diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css deleted file mode 100644 index 3e62df2329c..00000000000 --- a/vendor/assets/stylesheets/katex.css +++ /dev/null @@ -1,975 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Khan Academy - -This software also uses portions of the underscore.js project, which is -MIT licensed with the following copyright: - -Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative -Reporters & Editors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -/* - Here is how to build a version of KaTeX that works with gitlab. - - The problem is that the standard procedure for changing font location doesn't work for the empty string. - - 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. -*/ - -@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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - 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'); - font-weight: normal; - font-style: normal; -} -.katex-display { - display: block; - margin: 1em 0; - text-align: center; -} -.katex-display > .katex { - display: inline-block; - text-align: initial; -} -.katex { - font: normal 1.21em KaTeX_Main, Times New Roman, serif; - line-height: 1.2; - white-space: nowrap; - text-indent: 0; -} -.katex .katex-html { - display: inline-block; -} -.katex .katex-mathml { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; -} -.katex .base { - display: inline-block; -} -.katex .strut { - display: inline-block; -} -.katex .mathit { - font-family: KaTeX_Math; - font-style: italic; -} -.katex .mathbf { - font-family: KaTeX_Main; - font-weight: bold; -} -.katex .amsrm { - font-family: KaTeX_AMS; -} -.katex .mathbb { - font-family: KaTeX_AMS; -} -.katex .mathcal { - font-family: KaTeX_Caligraphic; -} -.katex .mathfrak { - font-family: KaTeX_Fraktur; -} -.katex .mathtt { - font-family: KaTeX_Typewriter; -} -.katex .mathscr { - font-family: KaTeX_Script; -} -.katex .mathsf { - font-family: KaTeX_SansSerif; -} -.katex .mainit { - font-family: KaTeX_Main; - font-style: italic; -} -.katex .textstyle > .mord + .mop { - margin-left: 0.16667em; -} -.katex .textstyle > .mord + .mbin { - margin-left: 0.22222em; -} -.katex .textstyle > .mord + .mrel { - margin-left: 0.27778em; -} -.katex .textstyle > .mord + .minner { - margin-left: 0.16667em; -} -.katex .textstyle > .mop + .mord { - margin-left: 0.16667em; -} -.katex .textstyle > .mop + .mop { - margin-left: 0.16667em; -} -.katex .textstyle > .mop + .mrel { - margin-left: 0.27778em; -} -.katex .textstyle > .mop + .minner { - margin-left: 0.16667em; -} -.katex .textstyle > .mbin + .mord { - margin-left: 0.22222em; -} -.katex .textstyle > .mbin + .mop { - margin-left: 0.22222em; -} -.katex .textstyle > .mbin + .mopen { - margin-left: 0.22222em; -} -.katex .textstyle > .mbin + .minner { - margin-left: 0.22222em; -} -.katex .textstyle > .mrel + .mord { - margin-left: 0.27778em; -} -.katex .textstyle > .mrel + .mop { - margin-left: 0.27778em; -} -.katex .textstyle > .mrel + .mopen { - margin-left: 0.27778em; -} -.katex .textstyle > .mrel + .minner { - margin-left: 0.27778em; -} -.katex .textstyle > .mclose + .mop { - margin-left: 0.16667em; -} -.katex .textstyle > .mclose + .mbin { - margin-left: 0.22222em; -} -.katex .textstyle > .mclose + .mrel { - margin-left: 0.27778em; -} -.katex .textstyle > .mclose + .minner { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mord { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mop { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mrel { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mopen { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mclose { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .mpunct { - margin-left: 0.16667em; -} -.katex .textstyle > .mpunct + .minner { - margin-left: 0.16667em; -} -.katex .textstyle > .minner + .mord { - margin-left: 0.16667em; -} -.katex .textstyle > .minner + .mop { - margin-left: 0.16667em; -} -.katex .textstyle > .minner + .mbin { - margin-left: 0.22222em; -} -.katex .textstyle > .minner + .mrel { - margin-left: 0.27778em; -} -.katex .textstyle > .minner + .mopen { - margin-left: 0.16667em; -} -.katex .textstyle > .minner + .mpunct { - margin-left: 0.16667em; -} -.katex .textstyle > .minner + .minner { - margin-left: 0.16667em; -} -.katex .mord + .mop { - margin-left: 0.16667em; -} -.katex .mop + .mord { - margin-left: 0.16667em; -} -.katex .mop + .mop { - margin-left: 0.16667em; -} -.katex .mclose + .mop { - margin-left: 0.16667em; -} -.katex .minner + .mop { - margin-left: 0.16667em; -} -.katex .reset-textstyle.textstyle { - font-size: 1em; -} -.katex .reset-textstyle.scriptstyle { - font-size: 0.7em; -} -.katex .reset-textstyle.scriptscriptstyle { - font-size: 0.5em; -} -.katex .reset-scriptstyle.textstyle { - font-size: 1.42857em; -} -.katex .reset-scriptstyle.scriptstyle { - font-size: 1em; -} -.katex .reset-scriptstyle.scriptscriptstyle { - font-size: 0.71429em; -} -.katex .reset-scriptscriptstyle.textstyle { - font-size: 2em; -} -.katex .reset-scriptscriptstyle.scriptstyle { - font-size: 1.4em; -} -.katex .reset-scriptscriptstyle.scriptscriptstyle { - font-size: 1em; -} -.katex .style-wrap { - position: relative; -} -.katex .vlist { - display: inline-block; -} -.katex .vlist > span { - display: block; - height: 0; - position: relative; -} -.katex .vlist > span > span { - display: inline-block; -} -.katex .vlist .baseline-fix { - display: inline-table; - table-layout: fixed; -} -.katex .msupsub { - text-align: left; -} -.katex .mfrac > span > span { - text-align: center; -} -.katex .mfrac .frac-line { - width: 100%; -} -.katex .mfrac .frac-line:before { - border-bottom-style: solid; - border-bottom-width: 1px; - content: ""; - display: block; -} -.katex .mfrac .frac-line:after { - border-bottom-style: solid; - border-bottom-width: 0.04em; - content: ""; - display: block; - margin-top: -1px; -} -.katex .mspace { - display: inline-block; -} -.katex .mspace.negativethinspace { - margin-left: -0.16667em; -} -.katex .mspace.thinspace { - width: 0.16667em; -} -.katex .mspace.mediumspace { - width: 0.22222em; -} -.katex .mspace.thickspace { - width: 0.27778em; -} -.katex .mspace.enspace { - width: 0.5em; -} -.katex .mspace.quad { - width: 1em; -} -.katex .mspace.qquad { - width: 2em; -} -.katex .llap, -.katex .rlap { - width: 0; - position: relative; -} -.katex .llap > .inner, -.katex .rlap > .inner { - position: absolute; -} -.katex .llap > .fix, -.katex .rlap > .fix { - display: inline-block; -} -.katex .llap > .inner { - right: 0; -} -.katex .rlap > .inner { - left: 0; -} -.katex .katex-logo .a { - font-size: 0.75em; - margin-left: -0.32em; - position: relative; - top: -0.2em; -} -.katex .katex-logo .t { - margin-left: -0.23em; -} -.katex .katex-logo .e { - margin-left: -0.1667em; - position: relative; - top: 0.2155em; -} -.katex .katex-logo .x { - margin-left: -0.125em; -} -.katex .rule { - display: inline-block; - border: solid 0; - position: relative; -} -.katex .overline .overline-line, -.katex .underline .underline-line { - width: 100%; -} -.katex .overline .overline-line:before, -.katex .underline .underline-line:before { - border-bottom-style: solid; - border-bottom-width: 1px; - content: ""; - display: block; -} -.katex .overline .overline-line:after, -.katex .underline .underline-line:after { - border-bottom-style: solid; - border-bottom-width: 0.04em; - content: ""; - display: block; - margin-top: -1px; -} -.katex .sqrt > .sqrt-sign { - position: relative; -} -.katex .sqrt .sqrt-line { - width: 100%; -} -.katex .sqrt .sqrt-line:before { - border-bottom-style: solid; - border-bottom-width: 1px; - content: ""; - display: block; -} -.katex .sqrt .sqrt-line:after { - border-bottom-style: solid; - border-bottom-width: 0.04em; - content: ""; - display: block; - margin-top: -1px; -} -.katex .sqrt > .root { - margin-left: 0.27777778em; - margin-right: -0.55555556em; -} -.katex .sizing, -.katex .fontsize-ensurer { - display: inline-block; -} -.katex .sizing.reset-size1.size1, -.katex .fontsize-ensurer.reset-size1.size1 { - font-size: 1em; -} -.katex .sizing.reset-size1.size2, -.katex .fontsize-ensurer.reset-size1.size2 { - font-size: 1.4em; -} -.katex .sizing.reset-size1.size3, -.katex .fontsize-ensurer.reset-size1.size3 { - font-size: 1.6em; -} -.katex .sizing.reset-size1.size4, -.katex .fontsize-ensurer.reset-size1.size4 { - font-size: 1.8em; -} -.katex .sizing.reset-size1.size5, -.katex .fontsize-ensurer.reset-size1.size5 { - font-size: 2em; -} -.katex .sizing.reset-size1.size6, -.katex .fontsize-ensurer.reset-size1.size6 { - font-size: 2.4em; -} -.katex .sizing.reset-size1.size7, -.katex .fontsize-ensurer.reset-size1.size7 { - font-size: 2.88em; -} -.katex .sizing.reset-size1.size8, -.katex .fontsize-ensurer.reset-size1.size8 { - font-size: 3.46em; -} -.katex .sizing.reset-size1.size9, -.katex .fontsize-ensurer.reset-size1.size9 { - font-size: 4.14em; -} -.katex .sizing.reset-size1.size10, -.katex .fontsize-ensurer.reset-size1.size10 { - font-size: 4.98em; -} -.katex .sizing.reset-size2.size1, -.katex .fontsize-ensurer.reset-size2.size1 { - font-size: 0.71428571em; -} -.katex .sizing.reset-size2.size2, -.katex .fontsize-ensurer.reset-size2.size2 { - font-size: 1em; -} -.katex .sizing.reset-size2.size3, -.katex .fontsize-ensurer.reset-size2.size3 { - font-size: 1.14285714em; -} -.katex .sizing.reset-size2.size4, -.katex .fontsize-ensurer.reset-size2.size4 { - font-size: 1.28571429em; -} -.katex .sizing.reset-size2.size5, -.katex .fontsize-ensurer.reset-size2.size5 { - font-size: 1.42857143em; -} -.katex .sizing.reset-size2.size6, -.katex .fontsize-ensurer.reset-size2.size6 { - font-size: 1.71428571em; -} -.katex .sizing.reset-size2.size7, -.katex .fontsize-ensurer.reset-size2.size7 { - font-size: 2.05714286em; -} -.katex .sizing.reset-size2.size8, -.katex .fontsize-ensurer.reset-size2.size8 { - font-size: 2.47142857em; -} -.katex .sizing.reset-size2.size9, -.katex .fontsize-ensurer.reset-size2.size9 { - font-size: 2.95714286em; -} -.katex .sizing.reset-size2.size10, -.katex .fontsize-ensurer.reset-size2.size10 { - font-size: 3.55714286em; -} -.katex .sizing.reset-size3.size1, -.katex .fontsize-ensurer.reset-size3.size1 { - font-size: 0.625em; -} -.katex .sizing.reset-size3.size2, -.katex .fontsize-ensurer.reset-size3.size2 { - font-size: 0.875em; -} -.katex .sizing.reset-size3.size3, -.katex .fontsize-ensurer.reset-size3.size3 { - font-size: 1em; -} -.katex .sizing.reset-size3.size4, -.katex .fontsize-ensurer.reset-size3.size4 { - font-size: 1.125em; -} -.katex .sizing.reset-size3.size5, -.katex .fontsize-ensurer.reset-size3.size5 { - font-size: 1.25em; -} -.katex .sizing.reset-size3.size6, -.katex .fontsize-ensurer.reset-size3.size6 { - font-size: 1.5em; -} -.katex .sizing.reset-size3.size7, -.katex .fontsize-ensurer.reset-size3.size7 { - font-size: 1.8em; -} -.katex .sizing.reset-size3.size8, -.katex .fontsize-ensurer.reset-size3.size8 { - font-size: 2.1625em; -} -.katex .sizing.reset-size3.size9, -.katex .fontsize-ensurer.reset-size3.size9 { - font-size: 2.5875em; -} -.katex .sizing.reset-size3.size10, -.katex .fontsize-ensurer.reset-size3.size10 { - font-size: 3.1125em; -} -.katex .sizing.reset-size4.size1, -.katex .fontsize-ensurer.reset-size4.size1 { - font-size: 0.55555556em; -} -.katex .sizing.reset-size4.size2, -.katex .fontsize-ensurer.reset-size4.size2 { - font-size: 0.77777778em; -} -.katex .sizing.reset-size4.size3, -.katex .fontsize-ensurer.reset-size4.size3 { - font-size: 0.88888889em; -} -.katex .sizing.reset-size4.size4, -.katex .fontsize-ensurer.reset-size4.size4 { - font-size: 1em; -} -.katex .sizing.reset-size4.size5, -.katex .fontsize-ensurer.reset-size4.size5 { - font-size: 1.11111111em; -} -.katex .sizing.reset-size4.size6, -.katex .fontsize-ensurer.reset-size4.size6 { - font-size: 1.33333333em; -} -.katex .sizing.reset-size4.size7, -.katex .fontsize-ensurer.reset-size4.size7 { - font-size: 1.6em; -} -.katex .sizing.reset-size4.size8, -.katex .fontsize-ensurer.reset-size4.size8 { - font-size: 1.92222222em; -} -.katex .sizing.reset-size4.size9, -.katex .fontsize-ensurer.reset-size4.size9 { - font-size: 2.3em; -} -.katex .sizing.reset-size4.size10, -.katex .fontsize-ensurer.reset-size4.size10 { - font-size: 2.76666667em; -} -.katex .sizing.reset-size5.size1, -.katex .fontsize-ensurer.reset-size5.size1 { - font-size: 0.5em; -} -.katex .sizing.reset-size5.size2, -.katex .fontsize-ensurer.reset-size5.size2 { - font-size: 0.7em; -} -.katex .sizing.reset-size5.size3, -.katex .fontsize-ensurer.reset-size5.size3 { - font-size: 0.8em; -} -.katex .sizing.reset-size5.size4, -.katex .fontsize-ensurer.reset-size5.size4 { - font-size: 0.9em; -} -.katex .sizing.reset-size5.size5, -.katex .fontsize-ensurer.reset-size5.size5 { - font-size: 1em; -} -.katex .sizing.reset-size5.size6, -.katex .fontsize-ensurer.reset-size5.size6 { - font-size: 1.2em; -} -.katex .sizing.reset-size5.size7, -.katex .fontsize-ensurer.reset-size5.size7 { - font-size: 1.44em; -} -.katex .sizing.reset-size5.size8, -.katex .fontsize-ensurer.reset-size5.size8 { - font-size: 1.73em; -} -.katex .sizing.reset-size5.size9, -.katex .fontsize-ensurer.reset-size5.size9 { - font-size: 2.07em; -} -.katex .sizing.reset-size5.size10, -.katex .fontsize-ensurer.reset-size5.size10 { - font-size: 2.49em; -} -.katex .sizing.reset-size6.size1, -.katex .fontsize-ensurer.reset-size6.size1 { - font-size: 0.41666667em; -} -.katex .sizing.reset-size6.size2, -.katex .fontsize-ensurer.reset-size6.size2 { - font-size: 0.58333333em; -} -.katex .sizing.reset-size6.size3, -.katex .fontsize-ensurer.reset-size6.size3 { - font-size: 0.66666667em; -} -.katex .sizing.reset-size6.size4, -.katex .fontsize-ensurer.reset-size6.size4 { - font-size: 0.75em; -} -.katex .sizing.reset-size6.size5, -.katex .fontsize-ensurer.reset-size6.size5 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size6.size6, -.katex .fontsize-ensurer.reset-size6.size6 { - font-size: 1em; -} -.katex .sizing.reset-size6.size7, -.katex .fontsize-ensurer.reset-size6.size7 { - font-size: 1.2em; -} -.katex .sizing.reset-size6.size8, -.katex .fontsize-ensurer.reset-size6.size8 { - font-size: 1.44166667em; -} -.katex .sizing.reset-size6.size9, -.katex .fontsize-ensurer.reset-size6.size9 { - font-size: 1.725em; -} -.katex .sizing.reset-size6.size10, -.katex .fontsize-ensurer.reset-size6.size10 { - font-size: 2.075em; -} -.katex .sizing.reset-size7.size1, -.katex .fontsize-ensurer.reset-size7.size1 { - font-size: 0.34722222em; -} -.katex .sizing.reset-size7.size2, -.katex .fontsize-ensurer.reset-size7.size2 { - font-size: 0.48611111em; -} -.katex .sizing.reset-size7.size3, -.katex .fontsize-ensurer.reset-size7.size3 { - font-size: 0.55555556em; -} -.katex .sizing.reset-size7.size4, -.katex .fontsize-ensurer.reset-size7.size4 { - font-size: 0.625em; -} -.katex .sizing.reset-size7.size5, -.katex .fontsize-ensurer.reset-size7.size5 { - font-size: 0.69444444em; -} -.katex .sizing.reset-size7.size6, -.katex .fontsize-ensurer.reset-size7.size6 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size7.size7, -.katex .fontsize-ensurer.reset-size7.size7 { - font-size: 1em; -} -.katex .sizing.reset-size7.size8, -.katex .fontsize-ensurer.reset-size7.size8 { - font-size: 1.20138889em; -} -.katex .sizing.reset-size7.size9, -.katex .fontsize-ensurer.reset-size7.size9 { - font-size: 1.4375em; -} -.katex .sizing.reset-size7.size10, -.katex .fontsize-ensurer.reset-size7.size10 { - font-size: 1.72916667em; -} -.katex .sizing.reset-size8.size1, -.katex .fontsize-ensurer.reset-size8.size1 { - font-size: 0.28901734em; -} -.katex .sizing.reset-size8.size2, -.katex .fontsize-ensurer.reset-size8.size2 { - font-size: 0.40462428em; -} -.katex .sizing.reset-size8.size3, -.katex .fontsize-ensurer.reset-size8.size3 { - font-size: 0.46242775em; -} -.katex .sizing.reset-size8.size4, -.katex .fontsize-ensurer.reset-size8.size4 { - font-size: 0.52023121em; -} -.katex .sizing.reset-size8.size5, -.katex .fontsize-ensurer.reset-size8.size5 { - font-size: 0.57803468em; -} -.katex .sizing.reset-size8.size6, -.katex .fontsize-ensurer.reset-size8.size6 { - font-size: 0.69364162em; -} -.katex .sizing.reset-size8.size7, -.katex .fontsize-ensurer.reset-size8.size7 { - font-size: 0.83236994em; -} -.katex .sizing.reset-size8.size8, -.katex .fontsize-ensurer.reset-size8.size8 { - font-size: 1em; -} -.katex .sizing.reset-size8.size9, -.katex .fontsize-ensurer.reset-size8.size9 { - font-size: 1.19653179em; -} -.katex .sizing.reset-size8.size10, -.katex .fontsize-ensurer.reset-size8.size10 { - font-size: 1.43930636em; -} -.katex .sizing.reset-size9.size1, -.katex .fontsize-ensurer.reset-size9.size1 { - font-size: 0.24154589em; -} -.katex .sizing.reset-size9.size2, -.katex .fontsize-ensurer.reset-size9.size2 { - font-size: 0.33816425em; -} -.katex .sizing.reset-size9.size3, -.katex .fontsize-ensurer.reset-size9.size3 { - font-size: 0.38647343em; -} -.katex .sizing.reset-size9.size4, -.katex .fontsize-ensurer.reset-size9.size4 { - font-size: 0.43478261em; -} -.katex .sizing.reset-size9.size5, -.katex .fontsize-ensurer.reset-size9.size5 { - font-size: 0.48309179em; -} -.katex .sizing.reset-size9.size6, -.katex .fontsize-ensurer.reset-size9.size6 { - font-size: 0.57971014em; -} -.katex .sizing.reset-size9.size7, -.katex .fontsize-ensurer.reset-size9.size7 { - font-size: 0.69565217em; -} -.katex .sizing.reset-size9.size8, -.katex .fontsize-ensurer.reset-size9.size8 { - font-size: 0.83574879em; -} -.katex .sizing.reset-size9.size9, -.katex .fontsize-ensurer.reset-size9.size9 { - font-size: 1em; -} -.katex .sizing.reset-size9.size10, -.katex .fontsize-ensurer.reset-size9.size10 { - font-size: 1.20289855em; -} -.katex .sizing.reset-size10.size1, -.katex .fontsize-ensurer.reset-size10.size1 { - font-size: 0.20080321em; -} -.katex .sizing.reset-size10.size2, -.katex .fontsize-ensurer.reset-size10.size2 { - font-size: 0.2811245em; -} -.katex .sizing.reset-size10.size3, -.katex .fontsize-ensurer.reset-size10.size3 { - font-size: 0.32128514em; -} -.katex .sizing.reset-size10.size4, -.katex .fontsize-ensurer.reset-size10.size4 { - font-size: 0.36144578em; -} -.katex .sizing.reset-size10.size5, -.katex .fontsize-ensurer.reset-size10.size5 { - font-size: 0.40160643em; -} -.katex .sizing.reset-size10.size6, -.katex .fontsize-ensurer.reset-size10.size6 { - font-size: 0.48192771em; -} -.katex .sizing.reset-size10.size7, -.katex .fontsize-ensurer.reset-size10.size7 { - font-size: 0.57831325em; -} -.katex .sizing.reset-size10.size8, -.katex .fontsize-ensurer.reset-size10.size8 { - font-size: 0.69477912em; -} -.katex .sizing.reset-size10.size9, -.katex .fontsize-ensurer.reset-size10.size9 { - font-size: 0.8313253em; -} -.katex .sizing.reset-size10.size10, -.katex .fontsize-ensurer.reset-size10.size10 { - font-size: 1em; -} -.katex .delimsizing.size1 { - font-family: KaTeX_Size1; -} -.katex .delimsizing.size2 { - font-family: KaTeX_Size2; -} -.katex .delimsizing.size3 { - font-family: KaTeX_Size3; -} -.katex .delimsizing.size4 { - font-family: KaTeX_Size4; -} -.katex .delimsizing.mult .delim-size1 > span { - font-family: KaTeX_Size1; -} -.katex .delimsizing.mult .delim-size4 > span { - font-family: KaTeX_Size4; -} -.katex .nulldelimiter { - display: inline-block; - width: 0.12em; -} -.katex .op-symbol { - position: relative; -} -.katex .op-symbol.small-op { - font-family: KaTeX_Size1; -} -.katex .op-symbol.large-op { - font-family: KaTeX_Size2; -} -.katex .op-limits > .vlist > span { - text-align: center; -} -.katex .accent > .vlist > span { - text-align: center; -} -.katex .accent .accent-body > span { - width: 0; -} -.katex .accent .accent-body.accent-vec > span { - position: relative; - left: 0.326em; -} -.katex .mtable .vertical-separator { - display: inline-block; - margin: 0 -0.025em; - border-right: 0.05em solid black; -} -.katex .mtable .arraycolsep { - display: inline-block; -} -.katex .mtable .col-align-c > .vlist { - text-align: center; -} -.katex .mtable .col-align-l > .vlist { - text-align: left; -} -.katex .mtable .col-align-r > .vlist { - text-align: right; -} diff --git a/vendor/assets/stylesheets/katex.scss b/vendor/assets/stylesheets/katex.scss new file mode 100644 index 00000000000..3e62df2329c --- /dev/null +++ b/vendor/assets/stylesheets/katex.scss @@ -0,0 +1,975 @@ +/* +The MIT License (MIT) + +Copyright (c) 2015 Khan Academy + +This software also uses portions of the underscore.js project, which is +MIT licensed with the following copyright: + +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + Here is how to build a version of KaTeX that works with gitlab. + + The problem is that the standard procedure for changing font location doesn't work for the empty string. + + 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. +*/ + +@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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + 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'); + font-weight: normal; + font-style: normal; +} +.katex-display { + display: block; + margin: 1em 0; + text-align: center; +} +.katex-display > .katex { + display: inline-block; + text-align: initial; +} +.katex { + font: normal 1.21em KaTeX_Main, Times New Roman, serif; + line-height: 1.2; + white-space: nowrap; + text-indent: 0; +} +.katex .katex-html { + display: inline-block; +} +.katex .katex-mathml { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.katex .base { + display: inline-block; +} +.katex .strut { + display: inline-block; +} +.katex .mathit { + font-family: KaTeX_Math; + font-style: italic; +} +.katex .mathbf { + font-family: KaTeX_Main; + font-weight: bold; +} +.katex .amsrm { + font-family: KaTeX_AMS; +} +.katex .mathbb { + font-family: KaTeX_AMS; +} +.katex .mathcal { + font-family: KaTeX_Caligraphic; +} +.katex .mathfrak { + font-family: KaTeX_Fraktur; +} +.katex .mathtt { + font-family: KaTeX_Typewriter; +} +.katex .mathscr { + font-family: KaTeX_Script; +} +.katex .mathsf { + font-family: KaTeX_SansSerif; +} +.katex .mainit { + font-family: KaTeX_Main; + font-style: italic; +} +.katex .textstyle > .mord + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mord + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mord + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mord + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mop + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mop + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mbin + .mord { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mop { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .mopen { + margin-left: 0.22222em; +} +.katex .textstyle > .mbin + .minner { + margin-left: 0.22222em; +} +.katex .textstyle > .mrel + .mord { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mop { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .mopen { + margin-left: 0.27778em; +} +.katex .textstyle > .mrel + .minner { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mclose + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .mclose + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .mclose + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mrel { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mclose { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .mpunct + .minner { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mord { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mop { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mbin { + margin-left: 0.22222em; +} +.katex .textstyle > .minner + .mrel { + margin-left: 0.27778em; +} +.katex .textstyle > .minner + .mopen { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .mpunct { + margin-left: 0.16667em; +} +.katex .textstyle > .minner + .minner { + margin-left: 0.16667em; +} +.katex .mord + .mop { + margin-left: 0.16667em; +} +.katex .mop + .mord { + margin-left: 0.16667em; +} +.katex .mop + .mop { + margin-left: 0.16667em; +} +.katex .mclose + .mop { + margin-left: 0.16667em; +} +.katex .minner + .mop { + margin-left: 0.16667em; +} +.katex .reset-textstyle.textstyle { + font-size: 1em; +} +.katex .reset-textstyle.scriptstyle { + font-size: 0.7em; +} +.katex .reset-textstyle.scriptscriptstyle { + font-size: 0.5em; +} +.katex .reset-scriptstyle.textstyle { + font-size: 1.42857em; +} +.katex .reset-scriptstyle.scriptstyle { + font-size: 1em; +} +.katex .reset-scriptstyle.scriptscriptstyle { + font-size: 0.71429em; +} +.katex .reset-scriptscriptstyle.textstyle { + font-size: 2em; +} +.katex .reset-scriptscriptstyle.scriptstyle { + font-size: 1.4em; +} +.katex .reset-scriptscriptstyle.scriptscriptstyle { + font-size: 1em; +} +.katex .style-wrap { + position: relative; +} +.katex .vlist { + display: inline-block; +} +.katex .vlist > span { + display: block; + height: 0; + position: relative; +} +.katex .vlist > span > span { + display: inline-block; +} +.katex .vlist .baseline-fix { + display: inline-table; + table-layout: fixed; +} +.katex .msupsub { + text-align: left; +} +.katex .mfrac > span > span { + text-align: center; +} +.katex .mfrac .frac-line { + width: 100%; +} +.katex .mfrac .frac-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .mfrac .frac-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .mspace { + display: inline-block; +} +.katex .mspace.negativethinspace { + margin-left: -0.16667em; +} +.katex .mspace.thinspace { + width: 0.16667em; +} +.katex .mspace.mediumspace { + width: 0.22222em; +} +.katex .mspace.thickspace { + width: 0.27778em; +} +.katex .mspace.enspace { + width: 0.5em; +} +.katex .mspace.quad { + width: 1em; +} +.katex .mspace.qquad { + width: 2em; +} +.katex .llap, +.katex .rlap { + width: 0; + position: relative; +} +.katex .llap > .inner, +.katex .rlap > .inner { + position: absolute; +} +.katex .llap > .fix, +.katex .rlap > .fix { + display: inline-block; +} +.katex .llap > .inner { + right: 0; +} +.katex .rlap > .inner { + left: 0; +} +.katex .katex-logo .a { + font-size: 0.75em; + margin-left: -0.32em; + position: relative; + top: -0.2em; +} +.katex .katex-logo .t { + margin-left: -0.23em; +} +.katex .katex-logo .e { + margin-left: -0.1667em; + position: relative; + top: 0.2155em; +} +.katex .katex-logo .x { + margin-left: -0.125em; +} +.katex .rule { + display: inline-block; + border: solid 0; + position: relative; +} +.katex .overline .overline-line, +.katex .underline .underline-line { + width: 100%; +} +.katex .overline .overline-line:before, +.katex .underline .underline-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .overline .overline-line:after, +.katex .underline .underline-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .sqrt-sign { + position: relative; +} +.katex .sqrt .sqrt-line { + width: 100%; +} +.katex .sqrt .sqrt-line:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; +} +.katex .sqrt .sqrt-line:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; +} +.katex .sqrt > .root { + margin-left: 0.27777778em; + margin-right: -0.55555556em; +} +.katex .sizing, +.katex .fontsize-ensurer { + display: inline-block; +} +.katex .sizing.reset-size1.size1, +.katex .fontsize-ensurer.reset-size1.size1 { + font-size: 1em; +} +.katex .sizing.reset-size1.size2, +.katex .fontsize-ensurer.reset-size1.size2 { + font-size: 1.4em; +} +.katex .sizing.reset-size1.size3, +.katex .fontsize-ensurer.reset-size1.size3 { + font-size: 1.6em; +} +.katex .sizing.reset-size1.size4, +.katex .fontsize-ensurer.reset-size1.size4 { + font-size: 1.8em; +} +.katex .sizing.reset-size1.size5, +.katex .fontsize-ensurer.reset-size1.size5 { + font-size: 2em; +} +.katex .sizing.reset-size1.size6, +.katex .fontsize-ensurer.reset-size1.size6 { + font-size: 2.4em; +} +.katex .sizing.reset-size1.size7, +.katex .fontsize-ensurer.reset-size1.size7 { + font-size: 2.88em; +} +.katex .sizing.reset-size1.size8, +.katex .fontsize-ensurer.reset-size1.size8 { + font-size: 3.46em; +} +.katex .sizing.reset-size1.size9, +.katex .fontsize-ensurer.reset-size1.size9 { + font-size: 4.14em; +} +.katex .sizing.reset-size1.size10, +.katex .fontsize-ensurer.reset-size1.size10 { + font-size: 4.98em; +} +.katex .sizing.reset-size2.size1, +.katex .fontsize-ensurer.reset-size2.size1 { + font-size: 0.71428571em; +} +.katex .sizing.reset-size2.size2, +.katex .fontsize-ensurer.reset-size2.size2 { + font-size: 1em; +} +.katex .sizing.reset-size2.size3, +.katex .fontsize-ensurer.reset-size2.size3 { + font-size: 1.14285714em; +} +.katex .sizing.reset-size2.size4, +.katex .fontsize-ensurer.reset-size2.size4 { + font-size: 1.28571429em; +} +.katex .sizing.reset-size2.size5, +.katex .fontsize-ensurer.reset-size2.size5 { + font-size: 1.42857143em; +} +.katex .sizing.reset-size2.size6, +.katex .fontsize-ensurer.reset-size2.size6 { + font-size: 1.71428571em; +} +.katex .sizing.reset-size2.size7, +.katex .fontsize-ensurer.reset-size2.size7 { + font-size: 2.05714286em; +} +.katex .sizing.reset-size2.size8, +.katex .fontsize-ensurer.reset-size2.size8 { + font-size: 2.47142857em; +} +.katex .sizing.reset-size2.size9, +.katex .fontsize-ensurer.reset-size2.size9 { + font-size: 2.95714286em; +} +.katex .sizing.reset-size2.size10, +.katex .fontsize-ensurer.reset-size2.size10 { + font-size: 3.55714286em; +} +.katex .sizing.reset-size3.size1, +.katex .fontsize-ensurer.reset-size3.size1 { + font-size: 0.625em; +} +.katex .sizing.reset-size3.size2, +.katex .fontsize-ensurer.reset-size3.size2 { + font-size: 0.875em; +} +.katex .sizing.reset-size3.size3, +.katex .fontsize-ensurer.reset-size3.size3 { + font-size: 1em; +} +.katex .sizing.reset-size3.size4, +.katex .fontsize-ensurer.reset-size3.size4 { + font-size: 1.125em; +} +.katex .sizing.reset-size3.size5, +.katex .fontsize-ensurer.reset-size3.size5 { + font-size: 1.25em; +} +.katex .sizing.reset-size3.size6, +.katex .fontsize-ensurer.reset-size3.size6 { + font-size: 1.5em; +} +.katex .sizing.reset-size3.size7, +.katex .fontsize-ensurer.reset-size3.size7 { + font-size: 1.8em; +} +.katex .sizing.reset-size3.size8, +.katex .fontsize-ensurer.reset-size3.size8 { + font-size: 2.1625em; +} +.katex .sizing.reset-size3.size9, +.katex .fontsize-ensurer.reset-size3.size9 { + font-size: 2.5875em; +} +.katex .sizing.reset-size3.size10, +.katex .fontsize-ensurer.reset-size3.size10 { + font-size: 3.1125em; +} +.katex .sizing.reset-size4.size1, +.katex .fontsize-ensurer.reset-size4.size1 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size4.size2, +.katex .fontsize-ensurer.reset-size4.size2 { + font-size: 0.77777778em; +} +.katex .sizing.reset-size4.size3, +.katex .fontsize-ensurer.reset-size4.size3 { + font-size: 0.88888889em; +} +.katex .sizing.reset-size4.size4, +.katex .fontsize-ensurer.reset-size4.size4 { + font-size: 1em; +} +.katex .sizing.reset-size4.size5, +.katex .fontsize-ensurer.reset-size4.size5 { + font-size: 1.11111111em; +} +.katex .sizing.reset-size4.size6, +.katex .fontsize-ensurer.reset-size4.size6 { + font-size: 1.33333333em; +} +.katex .sizing.reset-size4.size7, +.katex .fontsize-ensurer.reset-size4.size7 { + font-size: 1.6em; +} +.katex .sizing.reset-size4.size8, +.katex .fontsize-ensurer.reset-size4.size8 { + font-size: 1.92222222em; +} +.katex .sizing.reset-size4.size9, +.katex .fontsize-ensurer.reset-size4.size9 { + font-size: 2.3em; +} +.katex .sizing.reset-size4.size10, +.katex .fontsize-ensurer.reset-size4.size10 { + font-size: 2.76666667em; +} +.katex .sizing.reset-size5.size1, +.katex .fontsize-ensurer.reset-size5.size1 { + font-size: 0.5em; +} +.katex .sizing.reset-size5.size2, +.katex .fontsize-ensurer.reset-size5.size2 { + font-size: 0.7em; +} +.katex .sizing.reset-size5.size3, +.katex .fontsize-ensurer.reset-size5.size3 { + font-size: 0.8em; +} +.katex .sizing.reset-size5.size4, +.katex .fontsize-ensurer.reset-size5.size4 { + font-size: 0.9em; +} +.katex .sizing.reset-size5.size5, +.katex .fontsize-ensurer.reset-size5.size5 { + font-size: 1em; +} +.katex .sizing.reset-size5.size6, +.katex .fontsize-ensurer.reset-size5.size6 { + font-size: 1.2em; +} +.katex .sizing.reset-size5.size7, +.katex .fontsize-ensurer.reset-size5.size7 { + font-size: 1.44em; +} +.katex .sizing.reset-size5.size8, +.katex .fontsize-ensurer.reset-size5.size8 { + font-size: 1.73em; +} +.katex .sizing.reset-size5.size9, +.katex .fontsize-ensurer.reset-size5.size9 { + font-size: 2.07em; +} +.katex .sizing.reset-size5.size10, +.katex .fontsize-ensurer.reset-size5.size10 { + font-size: 2.49em; +} +.katex .sizing.reset-size6.size1, +.katex .fontsize-ensurer.reset-size6.size1 { + font-size: 0.41666667em; +} +.katex .sizing.reset-size6.size2, +.katex .fontsize-ensurer.reset-size6.size2 { + font-size: 0.58333333em; +} +.katex .sizing.reset-size6.size3, +.katex .fontsize-ensurer.reset-size6.size3 { + font-size: 0.66666667em; +} +.katex .sizing.reset-size6.size4, +.katex .fontsize-ensurer.reset-size6.size4 { + font-size: 0.75em; +} +.katex .sizing.reset-size6.size5, +.katex .fontsize-ensurer.reset-size6.size5 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size6.size6, +.katex .fontsize-ensurer.reset-size6.size6 { + font-size: 1em; +} +.katex .sizing.reset-size6.size7, +.katex .fontsize-ensurer.reset-size6.size7 { + font-size: 1.2em; +} +.katex .sizing.reset-size6.size8, +.katex .fontsize-ensurer.reset-size6.size8 { + font-size: 1.44166667em; +} +.katex .sizing.reset-size6.size9, +.katex .fontsize-ensurer.reset-size6.size9 { + font-size: 1.725em; +} +.katex .sizing.reset-size6.size10, +.katex .fontsize-ensurer.reset-size6.size10 { + font-size: 2.075em; +} +.katex .sizing.reset-size7.size1, +.katex .fontsize-ensurer.reset-size7.size1 { + font-size: 0.34722222em; +} +.katex .sizing.reset-size7.size2, +.katex .fontsize-ensurer.reset-size7.size2 { + font-size: 0.48611111em; +} +.katex .sizing.reset-size7.size3, +.katex .fontsize-ensurer.reset-size7.size3 { + font-size: 0.55555556em; +} +.katex .sizing.reset-size7.size4, +.katex .fontsize-ensurer.reset-size7.size4 { + font-size: 0.625em; +} +.katex .sizing.reset-size7.size5, +.katex .fontsize-ensurer.reset-size7.size5 { + font-size: 0.69444444em; +} +.katex .sizing.reset-size7.size6, +.katex .fontsize-ensurer.reset-size7.size6 { + font-size: 0.83333333em; +} +.katex .sizing.reset-size7.size7, +.katex .fontsize-ensurer.reset-size7.size7 { + font-size: 1em; +} +.katex .sizing.reset-size7.size8, +.katex .fontsize-ensurer.reset-size7.size8 { + font-size: 1.20138889em; +} +.katex .sizing.reset-size7.size9, +.katex .fontsize-ensurer.reset-size7.size9 { + font-size: 1.4375em; +} +.katex .sizing.reset-size7.size10, +.katex .fontsize-ensurer.reset-size7.size10 { + font-size: 1.72916667em; +} +.katex .sizing.reset-size8.size1, +.katex .fontsize-ensurer.reset-size8.size1 { + font-size: 0.28901734em; +} +.katex .sizing.reset-size8.size2, +.katex .fontsize-ensurer.reset-size8.size2 { + font-size: 0.40462428em; +} +.katex .sizing.reset-size8.size3, +.katex .fontsize-ensurer.reset-size8.size3 { + font-size: 0.46242775em; +} +.katex .sizing.reset-size8.size4, +.katex .fontsize-ensurer.reset-size8.size4 { + font-size: 0.52023121em; +} +.katex .sizing.reset-size8.size5, +.katex .fontsize-ensurer.reset-size8.size5 { + font-size: 0.57803468em; +} +.katex .sizing.reset-size8.size6, +.katex .fontsize-ensurer.reset-size8.size6 { + font-size: 0.69364162em; +} +.katex .sizing.reset-size8.size7, +.katex .fontsize-ensurer.reset-size8.size7 { + font-size: 0.83236994em; +} +.katex .sizing.reset-size8.size8, +.katex .fontsize-ensurer.reset-size8.size8 { + font-size: 1em; +} +.katex .sizing.reset-size8.size9, +.katex .fontsize-ensurer.reset-size8.size9 { + font-size: 1.19653179em; +} +.katex .sizing.reset-size8.size10, +.katex .fontsize-ensurer.reset-size8.size10 { + font-size: 1.43930636em; +} +.katex .sizing.reset-size9.size1, +.katex .fontsize-ensurer.reset-size9.size1 { + font-size: 0.24154589em; +} +.katex .sizing.reset-size9.size2, +.katex .fontsize-ensurer.reset-size9.size2 { + font-size: 0.33816425em; +} +.katex .sizing.reset-size9.size3, +.katex .fontsize-ensurer.reset-size9.size3 { + font-size: 0.38647343em; +} +.katex .sizing.reset-size9.size4, +.katex .fontsize-ensurer.reset-size9.size4 { + font-size: 0.43478261em; +} +.katex .sizing.reset-size9.size5, +.katex .fontsize-ensurer.reset-size9.size5 { + font-size: 0.48309179em; +} +.katex .sizing.reset-size9.size6, +.katex .fontsize-ensurer.reset-size9.size6 { + font-size: 0.57971014em; +} +.katex .sizing.reset-size9.size7, +.katex .fontsize-ensurer.reset-size9.size7 { + font-size: 0.69565217em; +} +.katex .sizing.reset-size9.size8, +.katex .fontsize-ensurer.reset-size9.size8 { + font-size: 0.83574879em; +} +.katex .sizing.reset-size9.size9, +.katex .fontsize-ensurer.reset-size9.size9 { + font-size: 1em; +} +.katex .sizing.reset-size9.size10, +.katex .fontsize-ensurer.reset-size9.size10 { + font-size: 1.20289855em; +} +.katex .sizing.reset-size10.size1, +.katex .fontsize-ensurer.reset-size10.size1 { + font-size: 0.20080321em; +} +.katex .sizing.reset-size10.size2, +.katex .fontsize-ensurer.reset-size10.size2 { + font-size: 0.2811245em; +} +.katex .sizing.reset-size10.size3, +.katex .fontsize-ensurer.reset-size10.size3 { + font-size: 0.32128514em; +} +.katex .sizing.reset-size10.size4, +.katex .fontsize-ensurer.reset-size10.size4 { + font-size: 0.36144578em; +} +.katex .sizing.reset-size10.size5, +.katex .fontsize-ensurer.reset-size10.size5 { + font-size: 0.40160643em; +} +.katex .sizing.reset-size10.size6, +.katex .fontsize-ensurer.reset-size10.size6 { + font-size: 0.48192771em; +} +.katex .sizing.reset-size10.size7, +.katex .fontsize-ensurer.reset-size10.size7 { + font-size: 0.57831325em; +} +.katex .sizing.reset-size10.size8, +.katex .fontsize-ensurer.reset-size10.size8 { + font-size: 0.69477912em; +} +.katex .sizing.reset-size10.size9, +.katex .fontsize-ensurer.reset-size10.size9 { + font-size: 0.8313253em; +} +.katex .sizing.reset-size10.size10, +.katex .fontsize-ensurer.reset-size10.size10 { + font-size: 1em; +} +.katex .delimsizing.size1 { + font-family: KaTeX_Size1; +} +.katex .delimsizing.size2 { + font-family: KaTeX_Size2; +} +.katex .delimsizing.size3 { + font-family: KaTeX_Size3; +} +.katex .delimsizing.size4 { + font-family: KaTeX_Size4; +} +.katex .delimsizing.mult .delim-size1 > span { + font-family: KaTeX_Size1; +} +.katex .delimsizing.mult .delim-size4 > span { + font-family: KaTeX_Size4; +} +.katex .nulldelimiter { + display: inline-block; + width: 0.12em; +} +.katex .op-symbol { + position: relative; +} +.katex .op-symbol.small-op { + font-family: KaTeX_Size1; +} +.katex .op-symbol.large-op { + font-family: KaTeX_Size2; +} +.katex .op-limits > .vlist > span { + text-align: center; +} +.katex .accent > .vlist > span { + text-align: center; +} +.katex .accent .accent-body > span { + width: 0; +} +.katex .accent .accent-body.accent-vec > span { + position: relative; + left: 0.326em; +} +.katex .mtable .vertical-separator { + display: inline-block; + margin: 0 -0.025em; + border-right: 0.05em solid black; +} +.katex .mtable .arraycolsep { + display: inline-block; +} +.katex .mtable .col-align-c > .vlist { + text-align: center; +} +.katex .mtable .col-align-l > .vlist { + text-align: left; +} +.katex .mtable .col-align-r > .vlist { + text-align: right; +} -- cgit v1.2.1 From 796c4c07fd2f0065763c43ca1998db4426a20ee5 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:24:15 +0900 Subject: 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; } -- cgit v1.2.1 From 21aefb44501041980b677822e1fce6a84f7be2fd Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Wed, 21 Dec 2016 23:37:59 +0900 Subject: 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' -- cgit v1.2.1 From 7afccbd121377c50b64f77ce0aebfeca512f4a7e Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Thu, 22 Dec 2016 00:14:36 +0900 Subject: 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 { -- cgit v1.2.1 From ffe78adf040ac96e0763bdf556ce19ac966e480c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 21 Dec 2016 13:51:17 -0800 Subject: 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: ``` -- cgit v1.2.1 From 12a088a15a88d2b2493847d9d33f6999f366b928 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Thu, 22 Dec 2016 00:33:38 +0100 Subject: 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 +} -- cgit v1.2.1 From 0a2fb360eeb38afef81b40e756663dd301543fad Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Dec 2016 22:18:41 -0500 Subject: 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| -- cgit v1.2.1