summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-05-07 11:59:43 +0900
committerShinya Maeda <shinya@gitlab.com>2018-05-07 11:59:43 +0900
commit1f39fcd1123c1a65798a0a0b3e5f3b2fa43651ac (patch)
tree8d8a6a5a6a424c3f61332e509b97ab85cf0167b5 /app
parentc1d3b48c96ce44a2ff3e84cb89063a00c67297f5 (diff)
parent58aa2d7f395be4aee38b5202ef1666879505c737 (diff)
downloadgitlab-ce-1f39fcd1123c1a65798a0a0b3e5f3b2fa43651ac.tar.gz
Merge branch 'master' into live-trace-v2
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js6
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/terms.scss55
-rw-r--r--app/assets/stylesheets/pages/repo.scss1
-rw-r--r--app/controllers/application_controller.rb33
-rw-r--r--app/controllers/concerns/continue_params.rb4
-rw-r--r--app/controllers/concerns/internal_redirect.rb35
-rw-r--r--app/controllers/import/base_controller.rb11
-rw-r--r--app/controllers/import/bitbucket_controller.rb6
-rw-r--r--app/controllers/import/fogbugz_controller.rb5
-rw-r--r--app/controllers/import/github_controller.rb5
-rw-r--r--app/controllers/import/gitlab_controller.rb5
-rw-r--r--app/controllers/import/google_code_controller.rb5
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb6
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb12
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/controllers/users/terms_controller.rb66
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/users_helper.rb33
-rw-r--r--app/models/application_setting.rb19
-rw-r--r--app/models/application_setting/term.rb13
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb70
-rw-r--r--app/models/ci/runner_namespace.rb9
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/namespace.rb3
-rw-r--r--app/models/project.rb179
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/models/project_import_state.rb55
-rw-r--r--app/models/term_agreement.rb6
-rw-r--r--app/models/user.rb6
-rw-r--r--app/policies/application_setting/term_policy.rb28
-rw-r--r--app/policies/user_policy.rb6
-rw-r--r--app/services/application_settings/update_service.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/register_job_service.rb29
-rw-r--r--app/services/ci/update_build_queue_service.rb16
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/users/respond_to_terms_service.rb24
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/views/admin/application_settings/_terms.html.haml22
-rw-r--r--app/views/admin/application_settings/show.html.haml111
-rw-r--r--app/views/admin/runners/_runner.html.haml6
-rw-r--r--app/views/admin/runners/index.html.haml3
-rw-r--r--app/views/admin/runners/show.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/layouts/_flash.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml22
-rw-r--r--app/views/layouts/header/_default.html.haml17
-rw-r--r--app/views/layouts/terms.html.haml34
-rw-r--r--app/views/projects/_import_project_pane.html.haml51
-rw-r--r--app/views/projects/new.html.haml53
-rw-r--r--app/views/projects/pipelines/new.html.haml18
-rw-r--r--app/views/projects/runners/_group_runners.html.haml32
-rw-r--r--app/views/projects/runners/_index.html.haml4
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/show.html.haml2
-rw-r--r--app/views/users/terms/index.html.haml13
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb9
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb5
-rw-r--r--app/workers/stuck_import_jobs_worker.rb9
65 files changed, 971 insertions, 270 deletions
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index bb8b3d91e40..90d4e19e90b 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit', this.handleSubmit);
+ this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 9aa8945e268..b0b077a5e4c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
});
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 360dcb6afef..9bd35183d8a 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -61,3 +61,4 @@
@import 'framework/stacked_progress_bar';
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
+@import 'framework/terms';
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
new file mode 100644
index 00000000000..dadfaf1c3f9
--- /dev/null
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -0,0 +1,55 @@
+.terms {
+ .alert-wrapper {
+ min-height: $header-height + $gl-padding;
+ }
+
+ .content {
+ padding-top: $gl-padding;
+ }
+
+ .panel {
+ .panel-heading {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .title {
+ display: flex;
+ align-items: center;
+
+ .logo-text {
+ width: 55px;
+ height: 24px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+ }
+
+ .navbar-collapse {
+ padding-right: 0;
+ }
+
+ .nav li a {
+ color: $theme-gray-700;
+ }
+ }
+
+ .panel-content {
+ padding: $gl-padding;
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .footer-block {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index e74606e864f..888757c12d8 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -440,6 +440,7 @@
padding-right: 3px;
.projects-sidebar {
+ min-height: 0;
display: flex;
flex-direction: column;
flex: 1;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 8ad13a82f89..2caffec66ac 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,12 +13,14 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
+ before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms },
+ unless: :peek_request?
before_action :validate_user_service_ticket!
before_action :check_password_expiration
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') }
+ before_action :add_gon_variables, unless: :peek_request?
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
@@ -269,6 +271,27 @@ class ApplicationController < ActionController::Base
end
end
+ def enforce_terms!
+ return unless current_user
+ return if current_user.terms_accepted?
+
+ if sessionless_user?
+ render_403
+ else
+ # Redirect to the destination if the request is a get.
+ # Redirect to the source if it was a post, so the user can re-submit after
+ # accepting the terms.
+ redirect_path = if request.get?
+ request.fullpath
+ else
+ URI(request.referer).path if request.referer
+ end
+
+ flash[:notice] = _("Please accept the Terms of Service before continuing.")
+ redirect_to terms_path(redirect: redirect_path), status: :found
+ end
+ end
+
def import_sources_enabled?
!Gitlab::CurrentSettings.import_sources.empty?
end
@@ -342,4 +365,12 @@ class ApplicationController < ActionController::Base
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
+
+ def sessionless_user?
+ current_user && !session.keys.include?('warden.user.user.key')
+ end
+
+ def peek_request?
+ request.path.start_with?('/-/peek')
+ end
end
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index eb3a623acdd..8b7355974df 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -1,4 +1,5 @@
module ContinueParams
+ include InternalRedirect
extend ActiveSupport::Concern
def continue_params
@@ -6,8 +7,7 @@ module ContinueParams
return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
- return unless continue_params[:to] && continue_params[:to].start_with?('/')
- return if continue_params[:to].start_with?('//')
+ continue_params[:to] = safe_redirect_path(continue_params[:to])
continue_params
end
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
new file mode 100644
index 00000000000..7409b2e89a5
--- /dev/null
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -0,0 +1,35 @@
+module InternalRedirect
+ extend ActiveSupport::Concern
+
+ def safe_redirect_path(path)
+ return unless path
+ # Verify that the string starts with a `/` but not a double `/`.
+ return unless path =~ %r{^/\w.*$}
+
+ uri = URI(path)
+ # Ignore anything path of the redirect except for the path, querystring and,
+ # fragment, forcing the redirect within the same host.
+ full_path_for_uri(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def safe_redirect_path_for_url(url)
+ return unless url
+
+ uri = URI(url)
+ safe_redirect_path(full_path_for_uri(uri)) if host_allowed?(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def host_allowed?(uri)
+ uri.host == request.host &&
+ uri.port == request.port
+ end
+
+ def full_path_for_uri(uri)
+ path_with_query = [uri.path, uri.query].compact.join('?')
+ [path_with_query, uri.fragment].compact.join("#")
+ end
+end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index c84fc2d305d..bcb856ce3f4 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,6 +1,17 @@
class Import::BaseController < ApplicationController
private
+ def find_already_added_projects(import_type)
+ current_user.created_projects.where(import_type: import_type).includes(:import_state)
+ end
+
+ def find_jobs(import_type)
+ current_user.created_projects
+ .includes(:import_state)
+ .where(import_type: import_type)
+ .to_json(only: [:id], methods: [:import_status])
+ end
+
def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 61d81ad8a71..77af5fb9c4f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
+ @already_added_projects = find_already_added_projects('bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- render json: current_user.created_projects
- .where(import_type: 'bitbucket')
- .to_json(only: [:id, :import_status])
+ render json: find_jobs('bitbucket')
end
def create
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 669eb31a995..25ec13b8075 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz')
+ @already_added_projects = find_already_added_projects('fogbugz')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('fogbugz')
end
def create
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index eb7d5fca367..f67ec4c248b 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: provider)
+ @already_added_projects = find_already_added_projects(provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs(provider)
end
def create
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 18f1d20f5a9..39e2e9e094b 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController
def status
@repos = client.projects
- @already_added_projects = current_user.created_projects.where(import_type: "gitlab")
+ @already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('gitlab')
end
def create
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index baa19fb383d..9b26a00f7c7 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController
@repos = client.repos
@incompatible_repos = client.incompatible_repos
- @already_added_projects = current_user.created_projects.where(import_type: "google_code")
+ @already_added_projects = find_already_added_projects('google_code')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('google_code')
end
def create
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 40d9fa18a10..ed89bed029b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -82,7 +82,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if identity_linker.changed?
redirect_identity_linked
- elsif identity_linker.error_message.present?
+ elsif identity_linker.failed?
redirect_identity_link_failed(identity_linker.error_message)
else
redirect_identity_exists
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 78d109cf33e..1ee273091d4 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -157,7 +157,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create_params
- params.require(:pipeline).permit(:ref)
+ params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
def pipeline
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index c950d0f7001..b9bbe7115c4 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController
redirect_to project_settings_ci_cd_path(@project)
end
+ def toggle_group_runners
+ project.toggle_ci_cd_settings!(:group_runners_enabled)
+
+ redirect_to project_settings_ci_cd_path(@project)
+ end
+
protected
def set_runner
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index d80ef8113aa..177c8a54099 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -67,10 +67,18 @@ module Projects
def define_runners_variables
@project_runners = @project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners
- .assignable_for(project).ordered.page(params[:page]).per(20)
+
+ @assignable_runners = current_user
+ .ci_authorized_runners
+ .assignable_for(project)
+ .ordered
+ .page(params[:page]).per(20)
+
@shared_runners = ::Ci::Runner.shared.active
+
@shared_runners_count = @shared_runners.count(:all)
+
+ @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end
def define_secret_variables
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index f3a4aa849c7..1a339f76d26 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,4 +1,5 @@
class SessionsController < Devise::SessionsController
+ include InternalRedirect
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
@@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController
# we should never redirect to '/users/sign_in' after signing in successfully.
return true if redirect_uri.path == new_user_session_path
- redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
+ redirect_to = redirect_uri.to_s if host_allowed?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end
- # Overridden in EE
- def redirect_allowed_to?(uri)
- uri.host == Gitlab.config.gitlab.host &&
- uri.port == Gitlab.config.gitlab.port
- end
-
def two_factor_enabled?
find_user&.two_factor_enabled?
end
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
new file mode 100644
index 00000000000..95c5c3432d5
--- /dev/null
+++ b/app/controllers/users/terms_controller.rb
@@ -0,0 +1,66 @@
+module Users
+ class TermsController < ApplicationController
+ include InternalRedirect
+
+ skip_before_action :enforce_terms!
+ before_action :terms
+
+ layout 'terms'
+
+ def index
+ @redirect = redirect_path
+ end
+
+ def accept
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: true)
+
+ if agreement.persisted?
+ redirect_to redirect_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ def decline
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: false)
+
+ if agreement.persisted?
+ sign_out(current_user)
+ redirect_to root_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ private
+
+ def viewed_term
+ @viewed_term ||= ApplicationSetting::Term.find(params[:id])
+ end
+
+ def terms
+ unless @term = Gitlab::CurrentSettings.current_application_settings.latest_terms
+ redirect_to redirect_path
+ end
+ end
+
+ def redirect_path
+ redirect_to_path = safe_redirect_path(params[:redirect]) || safe_redirect_path_for_url(request.referer)
+
+ if redirect_to_path &&
+ excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
+ redirect_to_path
+ else
+ root_path
+ end
+ end
+
+ def excluded_redirect_paths
+ [terms_path, new_user_session_path]
+ end
+ end
+end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3fbb32c5229..1bf98d550b0 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -248,7 +248,9 @@ module ApplicationSettingsHelper
:user_default_external,
:user_oauth_applications,
:version_check_enabled,
- :allow_local_requests_from_hooks_and_services
+ :allow_local_requests_from_hooks_and_services,
+ :enforce_terms,
+ :terms
]
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 01af68088df..e803cd3a8d8 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -23,9 +23,42 @@ module UsersHelper
profile_tabs.include?(tab)
end
+ def current_user_menu_items
+ @current_user_menu_items ||= get_current_user_menu_items
+ end
+
+ def current_user_menu?(item)
+ current_user_menu_items.include?(item)
+ end
+
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
end
+
+ def get_current_user_menu_items
+ items = []
+
+ items << :sign_out if current_user
+
+ # TODO: Remove these conditions when the permissions are prevented in
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849
+ terms_not_enforced = !Gitlab::CurrentSettings
+ .current_application_settings
+ .enforce_terms?
+ required_terms_accepted = terms_not_enforced || current_user.terms_accepted?
+
+ items << :help if required_terms_accepted
+
+ if can?(current_user, :read_user, current_user) && required_terms_accepted
+ items << :profile
+ end
+
+ if can?(current_user, :update_user, current_user) && required_terms_accepted
+ items << :settings
+ end
+
+ items
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 862933bf127..a734cc7a26b 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -220,12 +220,15 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validate :terms_exist, if: :enforce_terms?
+
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
after_commit do
+ reset_memoized_terms
Rails.cache.write(CACHE_KEY, self)
end
@@ -507,6 +510,16 @@ class ApplicationSetting < ActiveRecord::Base
password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
end
+ delegate :terms, to: :latest_terms, allow_nil: true
+ def latest_terms
+ @latest_terms ||= Term.latest
+ end
+
+ def reset_memoized_terms
+ @latest_terms = nil
+ latest_terms
+ end
+
private
def ensure_uuid!
@@ -520,4 +533,10 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
invalid.empty?
end
+
+ def terms_exist
+ return unless enforce_terms?
+
+ errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
+ end
end
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
new file mode 100644
index 00000000000..e8ce0ccbb71
--- /dev/null
+++ b/app/models/application_setting/term.rb
@@ -0,0 +1,13 @@
+class ApplicationSetting
+ class Term < ActiveRecord::Base
+ include CacheMarkdownField
+
+ validates :terms, presence: true
+
+ cache_markdown_field :terms
+
+ def self.latest
+ order(:id).last
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index e1b9bc76475..c184f398f6c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -32,6 +32,8 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+ accepts_nested_attributes_for :variables, reject_if: :persisted?
+
delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index de5aae17a15..38e14ffbc0c 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 5a4c56ec0dc..23078f1c3ed 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -14,31 +14,49 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
+ has_many :runner_namespaces
+ has_many :groups, through: :runner_namespaces
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values
- scope :specific, ->() { where(is_shared: false) }
- scope :shared, ->() { where(is_shared: true) }
- scope :active, ->() { where(active: true) }
- scope :paused, ->() { where(active: false) }
- scope :online, ->() { where('contacted_at > ?', contact_time_deadline) }
- scope :ordered, ->() { order(id: :desc) }
+ scope :specific, -> { where(is_shared: false) }
+ scope :shared, -> { where(is_shared: true) }
+ scope :active, -> { where(active: true) }
+ scope :paused, -> { where(active: false) }
+ scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
+ scope :ordered, -> { order(id: :desc) }
- scope :owned_or_shared, ->(project_id) do
- joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ scope :belonging_to_project, -> (project_id) {
+ joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
+ }
+
+ scope :belonging_to_parent_group_of_project, -> (project_id) {
+ project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
+ hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
+
+ joins(:groups).where(namespaces: { id: hierarchy_groups })
+ }
+
+ scope :owned_or_shared, -> (project_id) do
+ union = Gitlab::SQL::Union.new(
+ [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
+ remove_duplicates: false
+ )
+ from("(#{union.to_sql}) ci_runners")
end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
where(locked: false)
- .where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
+ .specific
end
validate :tag_constraints
+ validate :either_projects_or_group
validates :access_level, presence: true
acts_as_taggable
@@ -50,6 +68,12 @@ module Ci
ref_protected: 1
}
+ enum runner_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
@@ -120,6 +144,14 @@ module Ci
!shared?
end
+ def assigned_to_group?
+ runner_namespaces.any?
+ end
+
+ def assigned_to_project?
+ runner_projects.any?
+ end
+
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
@@ -174,6 +206,12 @@ module Ci
end
end
+ def pick_build!(build)
+ if can_pick?(build)
+ tick_runner_queue
+ end
+ end
+
private
def cleanup_runner_queue
@@ -205,7 +243,17 @@ module Ci
end
def assignable_for?(project_id)
- is_shared? || projects.exists?(id: project_id)
+ self.class.owned_or_shared(project_id).where(id: self.id).any?
+ end
+
+ def either_projects_or_group
+ if groups.many?
+ errors.add(:runner, 'can only be assigned to one group')
+ end
+
+ if assigned_to_group? && assigned_to_project?
+ errors.add(:runner, 'can only be assigned either to projects or to a group')
+ end
end
def accepting_tags?(build)
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
new file mode 100644
index 00000000000..3269f86e8ca
--- /dev/null
+++ b/app/models/ci/runner_namespace.rb
@@ -0,0 +1,9 @@
+module Ci
+ class RunnerNamespace < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :runner
+ belongs_to :namespace, class_name: '::Namespace'
+ belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9b42bbf99be..f493836a92e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -9,6 +9,7 @@ class Group < Namespace
include SelectForProjectAuthorization
include LoadedInGroupList
include GroupDescendant
+ include TokenAuthenticatable
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -43,6 +44,8 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ add_authentication_token_field :runners_token
+
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
@@ -294,6 +297,13 @@ class Group < Namespace
refresh_members_authorized_projects(blocking: false)
end
+ # each existing group needs to have a `runners_token`.
+ # we do this on read since migrating all existing groups is not a feasible
+ # solution.
+ def runners_token
+ ensure_runners_token!
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index c29a53e5ce7..5621eeba7c4 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
+ has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
+ has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
diff --git a/app/models/project.rb b/app/models/project.rb
index d4e9e51c7be..b12b694aabd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -67,6 +67,9 @@ class Project < ActiveRecord::Base
before_save :ensure_runners_token
after_save :update_project_statistics, if: :namespace_id_changed?
+
+ after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
+
after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
@@ -157,6 +160,8 @@ class Project < ActiveRecord::Base
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
+ has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
+
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
@@ -230,13 +235,11 @@ class Project < ActiveRecord::Base
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
- has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
-
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge'
- has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
+ has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -247,6 +250,7 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
+ delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
# Validations
validates :creator, presence: true, on: :create
@@ -332,6 +336,11 @@ class Project < ActiveRecord::Base
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
+ scope :with_group_runners_enabled, -> do
+ joins(:ci_cd_settings)
+ .where(project_ci_cd_settings: { group_runners_enabled: true })
+ end
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
@@ -381,55 +390,9 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
- scope :import_started, -> { where(import_status: 'started') }
- state_machine :import_status, initial: :none do
- event :import_schedule do
- transition [:none, :finished, :failed] => :scheduled
- end
-
- event :force_import_start do
- transition [:none, :finished, :failed] => :started
- end
-
- event :import_start do
- transition scheduled: :started
- end
-
- event :import_finish do
- transition started: :finished
- end
-
- event :import_fail do
- transition [:scheduled, :started] => :failed
- end
-
- event :import_retry do
- transition failed: :started
- end
-
- state :scheduled
- state :started
- state :finished
- state :failed
-
- after_transition [:none, :finished, :failed] => :scheduled do |project, _|
- project.run_after_commit do
- job_id = add_import_job
- update(import_jid: job_id) if job_id
- end
- end
-
- after_transition started: :finished do |project, _|
- project.reset_cache_and_import_attrs
-
- if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
- project.run_after_commit do
- Projects::AfterImportService.new(project).execute
- end
- end
- end
- end
+ scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
+ scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
class << self
# Searches for a list of projects based on the query given in `query`.
@@ -659,10 +622,6 @@ class Project < ActiveRecord::Base
external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
- def no_import?
- import_status == 'none'
- end
-
def external_import?
import_url.present?
end
@@ -675,6 +634,93 @@ class Project < ActiveRecord::Base
import_started? || import_scheduled?
end
+ def import_state_args
+ {
+ status: self[:import_status],
+ jid: self[:import_jid],
+ last_error: self[:import_error]
+ }
+ end
+
+ def ensure_import_state
+ return if self[:import_status] == 'none' || self[:import_status].nil?
+ return unless import_state.nil?
+
+ create_import_state(import_state_args)
+
+ update_column(:import_status, 'none')
+ end
+
+ def import_schedule
+ ensure_import_state
+
+ import_state&.schedule
+ end
+
+ def force_import_start
+ ensure_import_state
+
+ import_state&.force_start
+ end
+
+ def import_start
+ ensure_import_state
+
+ import_state&.start
+ end
+
+ def import_fail
+ ensure_import_state
+
+ import_state&.fail_op
+ end
+
+ def import_finish
+ ensure_import_state
+
+ import_state&.finish
+ end
+
+ def import_jid=(new_jid)
+ ensure_import_state
+
+ import_state&.jid = new_jid
+ end
+
+ def import_jid
+ ensure_import_state
+
+ import_state&.jid
+ end
+
+ def import_error=(new_error)
+ ensure_import_state
+
+ import_state&.last_error = new_error
+ end
+
+ def import_error
+ ensure_import_state
+
+ import_state&.last_error
+ end
+
+ def import_status=(new_status)
+ ensure_import_state
+
+ import_state&.status = new_status
+ end
+
+ def import_status
+ ensure_import_state
+
+ import_state&.status || 'none'
+ end
+
+ def no_import?
+ import_status == 'none'
+ end
+
def import_started?
# import? does SQL work so only run it if it looks like there's an import running
import_status == 'started' && import?
@@ -1301,12 +1347,17 @@ class Project < ActiveRecord::Base
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end
- def active_shared_runners
- @active_shared_runners ||= shared_runners.active
+ def group_runners
+ @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
+ end
+
+ def all_runners
+ union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
+ Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
def any_runners?(&block)
- active_runners.any?(&block) || active_shared_runners.any?(&block)
+ all_runners.active.any?(&block)
end
def valid_runners_token?(token)
@@ -1471,7 +1522,7 @@ class Project < ActiveRecord::Base
def rename_repo_notify!
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
- send_move_instructions(full_path_was) unless started?
+ send_move_instructions(full_path_was) unless import_started?
expires_full_path_cache
self.old_path_with_namespace = full_path_was
@@ -1525,7 +1576,8 @@ class Project < ActiveRecord::Base
return unless import_jid
Gitlab::SidekiqStatus.unset(import_jid)
- update_column(:import_jid, nil)
+
+ import_state.update_column(:jid, nil)
end
def running_or_pending_build_count(force: false)
@@ -1544,7 +1596,8 @@ class Project < ActiveRecord::Base
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
import_fail
- update_column(:import_error, sanitized_message)
+
+ import_state.update_column(:last_error, sanitized_message)
rescue ActiveRecord::ActiveRecordError => e
Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
ensure
@@ -1874,6 +1927,10 @@ class Project < ActiveRecord::Base
[]
end
+ def toggle_ci_cd_settings!(settings_attribute)
+ ci_cd_settings.toggle!(settings_attribute)
+ end
+
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 9f10a93148c..588cced5781 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,5 +1,5 @@
class ProjectCiCdSetting < ActiveRecord::Base
- belongs_to :project
+ belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
new file mode 100644
index 00000000000..1605317ae14
--- /dev/null
+++ b/app/models/project_import_state.rb
@@ -0,0 +1,55 @@
+class ProjectImportState < ActiveRecord::Base
+ include AfterCommitQueue
+
+ self.table_name = "project_mirror_data"
+
+ belongs_to :project, inverse_of: :import_state
+
+ validates :project, presence: true
+
+ state_machine :status, initial: :none do
+ event :schedule do
+ transition [:none, :finished, :failed] => :scheduled
+ end
+
+ event :force_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :start do
+ transition scheduled: :started
+ end
+
+ event :finish do
+ transition started: :finished
+ end
+
+ event :fail_op do
+ transition [:scheduled, :started] => :failed
+ end
+
+ state :scheduled
+ state :started
+ state :finished
+ state :failed
+
+ after_transition [:none, :finished, :failed] => :scheduled do |state, _|
+ state.run_after_commit do
+ job_id = project.add_import_job
+ update(jid: job_id) if job_id
+ end
+ end
+
+ after_transition started: :finished do |state, _|
+ project = state.project
+
+ project.reset_cache_and_import_attrs
+
+ if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
+ state.run_after_commit do
+ Projects::AfterImportService.new(project).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb
new file mode 100644
index 00000000000..8458a231bbd
--- /dev/null
+++ b/app/models/term_agreement.rb
@@ -0,0 +1,6 @@
+class TermAgreement < ActiveRecord::Base
+ belongs_to :term, class_name: 'ApplicationSetting::Term'
+ belongs_to :user
+
+ validates :user, :term, presence: true
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4a602ffbb05..a9cfd39f604 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -138,6 +138,8 @@ class User < ActiveRecord::Base
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :term_agreements
+ belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
#
# Validations
@@ -1187,6 +1189,10 @@ class User < ActiveRecord::Base
max_member_access_for_group_ids([group_id])[group_id]
end
+ def terms_accepted?
+ accepted_term_id.present?
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb
new file mode 100644
index 00000000000..f03bf748c76
--- /dev/null
+++ b/app/policies/application_setting/term_policy.rb
@@ -0,0 +1,28 @@
+class ApplicationSetting
+ class TermPolicy < BasePolicy
+ include Gitlab::Utils::StrongMemoize
+
+ condition(:current_terms, scope: :subject) do
+ Gitlab::CurrentSettings.current_application_settings.latest_terms == @subject
+ end
+
+ condition(:terms_accepted, score: 1) do
+ agreement&.accepted
+ end
+
+ rule { ~anonymous & current_terms }.policy do
+ enable :accept_terms
+ enable :decline_terms
+ end
+
+ rule { terms_accepted }.prevent :accept_terms
+
+ def agreement
+ strong_memoize(:agreement) do
+ next nil if @user.nil? || @subject.nil?
+
+ @user.term_agreements.find_by(term: @subject)
+ end
+ end
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 0905ddd9b38..ee219f0a0d0 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -8,6 +8,8 @@ class UserPolicy < BasePolicy
rule { ~restricted_public_level }.enable :read_user
rule { ~anonymous }.enable :read_user
- rule { user_is_self | admin }.enable :destroy_user
- rule { subject_ghost }.prevent :destroy_user
+ rule { ~subject_ghost & (user_is_self | admin) }.policy do
+ enable :destroy_user
+ enable :update_user
+ end
end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 61589a07250..d6d3a661dab 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -1,7 +1,22 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
+ update_terms(@params.delete(:terms))
+
@application_setting.update(@params)
end
+
+ private
+
+ def update_terms(terms)
+ return unless terms.present?
+
+ # Avoid creating a new terms record if the text is exactly the same.
+ terms = terms.strip
+ return if terms == @application_setting.terms
+
+ ApplicationSetting::Term.create(terms: terms)
+ @application_setting.reset_memoized_terms
+ end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 6ce86983287..17a53b6a8fd 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -24,6 +24,7 @@ module Ci
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
+ variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0b087ad73da..4291631913a 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -17,8 +17,10 @@ module Ci
builds =
if runner.shared?
builds_for_shared_runner
+ elsif runner.group_type?
+ builds_for_group_runner
else
- builds_for_specific_runner
+ builds_for_project_runner
end
valid = true
@@ -75,15 +77,24 @@ module Ci
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
- # Implement fair scheduling
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
- def builds_for_specific_runner
- new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
+ def builds_for_project_runner
+ new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
+ end
+
+ def builds_for_group_runner
+ hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
+ projects = Project.where(namespace_id: hierarchy_groups)
+ .with_group_runners_enabled
+ .with_builds_enabled
+ .without_deleted
+ new_builds.where(project: projects).order('id ASC')
end
def running_builds_for_shared_runners
@@ -97,10 +108,6 @@ module Ci
builds
end
- def shared_runner_build_limits_feature_enabled?
- ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
- end
-
def register_failure
failed_attempt_counter.increment
attempt_counter.increment
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
index 152c8ae5006..41b1c144c3e 100644
--- a/app/services/ci/update_build_queue_service.rb
+++ b/app/services/ci/update_build_queue_service.rb
@@ -1,18 +1,14 @@
module Ci
class UpdateBuildQueueService
def execute(build)
- build.project.runners.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
- end
+ tick_for(build, build.project.all_runners)
+ end
- return unless build.project.shared_runners_enabled?
+ private
- Ci::Runner.shared.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
+ def tick_for(build, runners)
+ runners.each do |runner|
+ runner.pick_build!(build)
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index d361d070993..d16ecdb7b9b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -142,7 +142,7 @@ module Projects
if @project
@project.errors.add(:base, message)
- @project.mark_import_as_failed(message) if @project.import?
+ @project.mark_import_as_failed(message) if @project.persisted? && @project.import?
end
@project
diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb
new file mode 100644
index 00000000000..06d660186cf
--- /dev/null
+++ b/app/services/users/respond_to_terms_service.rb
@@ -0,0 +1,24 @@
+module Users
+ class RespondToTermsService
+ def initialize(user, term)
+ @user, @term = user, term
+ end
+
+ def execute(accepted:)
+ agreement = @user.term_agreements.find_or_initialize_by(term: @term)
+ agreement.accepted = accepted
+
+ if agreement.save
+ store_accepted_term(accepted)
+ end
+
+ agreement
+ end
+
+ private
+
+ def store_accepted_term(accepted)
+ @user.update_column(:accepted_term_id, accepted ? @term.id : nil)
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 809ce1303d8..7ec52b6ce2b 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -41,7 +41,7 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError => e
log_execution(
trigger: hook_name,
url: hook.url,
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
new file mode 100644
index 00000000000..724246ab7e7
--- /dev/null
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-12
+ .checkbox
+ = f.label :enforce_terms do
+ = f.check_box :enforce_terms
+ = _("Require all users to accept Terms of Service when they access GitLab.")
+ .help-block
+ = _("When enabled, users cannot use GitLab until the terms have been accepted.")
+ .form-group
+ .col-sm-12
+ = f.label :terms do
+ = _("Terms of Service Agreement")
+ .col-sm-12
+ = f.text_area :terms, class: 'form-control', rows: 8
+ .help-block
+ = _("Markdown enabled")
+
+ = f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index caaa93aa1e2..3c00e3c8fc4 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -8,7 +8,7 @@
%h4
= _('Visibility and access controls')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
.settings-content
@@ -19,7 +19,7 @@
%h4
= _('Account and limit settings')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Session expiration, projects limit and attachment size.')
.settings-content
@@ -30,7 +30,7 @@
%h4
= _('Sign-up restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure the way a user creates a new account.')
.settings-content
@@ -41,18 +41,29 @@
%h4
= _('Sign-in restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
.settings-content
= render 'signin'
+%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Terms of Service')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Include a Terms of Service agreement that all users must accept.')
+ .settings-content
+ = render 'terms'
+
%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Help page')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Help page text and support page url.')
.settings-content
@@ -62,8 +73,8 @@
.settings-header
%h4
= _('Pages')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Size and domain settings for static websites')
.settings-content
@@ -73,8 +84,8 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Auto DevOps, runners and job artifacts')
.settings-content
@@ -84,8 +95,8 @@
.settings-header
%h4
= _('Metrics - Influx')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure InfluxDB metrics.')
.settings-content
@@ -95,8 +106,8 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
@@ -106,8 +117,8 @@
.settings-header
%h4
= _('Profiling - Performance bar')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable the Performance Bar for a given group.')
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
@@ -118,8 +129,8 @@
.settings-header
%h4
= _('Background jobs')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Sidekiq job throttling.')
.settings-content
@@ -129,8 +140,8 @@
.settings-header
%h4
= _('Spam and Anti-bot Protection')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable reCAPTCHA or Akismet and set IP limits.')
.settings-content
@@ -140,8 +151,8 @@
.settings-header
%h4
= _('Abuse reports')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set notification email for abuse reports.')
.settings-content
@@ -151,8 +162,8 @@
.settings-header
%h4
= _('Error Reporting and Logging')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable Sentry for error reporting and logging.')
.settings-content
@@ -162,8 +173,8 @@
.settings-header
%h4
= _('Repository storage')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure storage path and circuit breaker settings.')
.settings-content
@@ -173,8 +184,8 @@
.settings-header
%h4
= _('Repository maintenance')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
@@ -185,8 +196,8 @@
.settings-header
%h4
= _('Container Registry')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various container registry settings.')
.settings-content
@@ -197,8 +208,8 @@
.settings-header
%h4
= _('Koding')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Online IDE integration settings.')
.settings-content
@@ -208,8 +219,8 @@
.settings-header
%h4
= _('PlantUML')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
@@ -219,8 +230,8 @@
.settings-header#usage-statistics
%h4
= _('Usage statistics')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and usage ping.')
.settings-content
@@ -230,8 +241,8 @@
.settings-header
%h4
= _('Email')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various email settings.')
.settings-content
@@ -241,8 +252,8 @@
.settings-header
%h4
= _('Gitaly')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Gitaly timeouts.')
.settings-content
@@ -252,8 +263,8 @@
.settings-header
%h4
= _('Web terminal')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set max session time for web terminal.')
.settings-content
@@ -263,8 +274,8 @@
.settings-header
%h4
= _('Real-time features')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
.settings-content
@@ -274,8 +285,8 @@
.settings-header
%h4
= _('Performance optimization')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various settings that affect GitLab performance.')
.settings-content
@@ -285,8 +296,8 @@
.settings-header
%h4
= _('User and IP Rate Limits')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure limits for web and API requests.')
.settings-content
@@ -296,8 +307,8 @@
.settings-header
%h4
= _('Outbound requests')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow requests to the local network from hooks and services.')
.settings-content
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index f90b8b8c0a4..6e76e7c2768 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -2,6 +2,8 @@
%td
- if runner.shared?
%span.label.label-success shared
+ - elsif runner.group_type?
+ %span.label.label-success group
- else
%span.label.label-info specific
- if runner.locked?
@@ -19,7 +21,7 @@
%td
= runner.ip_address
%td
- - if runner.shared?
+ - if runner.shared? || runner.group_type?
n/a
- else
= runner.projects.count(:all)
@@ -31,7 +33,7 @@
= tag
%td
- if runner.contacted_at
- = time_ago_with_tooltip runner.contacted_at
+ #{time_ago_in_words(runner.contacted_at)} ago
- else
Never
%td.admin-runner-btn-group-cell
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 9f13dbbbd82..1a3b5e58ed5 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -17,6 +17,9 @@
%span.label.label-success shared
\- Runner runs jobs from all unassigned projects
%li
+ %span.label.label-success group
+ \- Runner runs jobs from all unassigned projects in its group
+ %li
%span.label.label-info specific
\- Runner runs jobs from assigned projects
%li
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index d04cf48b05c..d022016f70d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -19,6 +19,9 @@
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
+- elsif @runner.group_type?
+ .bs-callout.bs-callout-success
+ %h4 This runner will process jobs from all projects in its group and subgroups
- else
.bs-callout.bs-callout-info
%h4 This Runner will process jobs only from ASSIGNED projects
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 0ef4b71f4fe..10b8bf5d565 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -42,31 +42,31 @@
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
- %small.badge= number_with_delimiter(User.active.count)
+ %small.badge= limited_counter_with_delimiter(User.active)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
- %small.badge= number_with_delimiter(User.admins.count)
+ %small.badge= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
- %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %small.badge= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
- %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %small.badge= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
- %small.badge= number_with_delimiter(User.external.count)
+ %small.badge= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.badge= number_with_delimiter(User.blocked.count)
+ %small.badge= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.badge= number_with_delimiter(User.without_projects.count)
+ %small.badge= limited_counter_with_delimiter(User.without_projects)
%ul.flex-list.content-list
- if @users.empty?
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 05ddd0ec733..8bd5708d490 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
+- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
+
.flash-container.flash-container-page
-# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
-# Don't show a flash message if the message is nil
- if value
%div{ class: "flash-#{key}" }
- %div{ class: (container_class) }
+ %div{ class: "#{container_class} #{extra_flash_class}" }
%span= value
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
new file mode 100644
index 00000000000..24b6c490a5a
--- /dev/null
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -0,0 +1,22 @@
+- return unless current_user
+
+%ul
+ %li.current-user
+ .user-name.bold
+ = current_user.name
+ = current_user.to_reference
+ %li.divider
+ - if current_user_menu?(:profile)
+ %li
+ = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
+ - if current_user_menu?(:settings)
+ %li
+ = link_to s_("CurrentUser|Settings"), profile_path
+ - if current_user_menu?(:help)
+ %li
+ = link_to _("Help"), help_path
+ - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
+ %li.divider
+ - if current_user_menu?(:sign_out)
+ %li
+ = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e6238c0dddb..dc121812406 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -53,22 +53,7 @@
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
- %ul
- %li.current-user
- .user-name.bold
- = current_user.name
- @#{current_user.username}
- %li.divider
- %li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
- %li
- = link_to "Settings", profile_path
- - if current_user
- %li
- = link_to "Help", help_path
- %li.divider
- %li
- = link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
+ = render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
%li.impersonation
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
new file mode 100644
index 00000000000..a30d6e2688c
--- /dev/null
+++ b/app/views/layouts/terms.html.haml
@@ -0,0 +1,34 @@
+!!! 5
+- @hide_breadcrumbs = true
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+
+ %body{ data: { page: body_data_page } }
+ .layout-page.terms{ class: page_class }
+ .content-wrapper.prepend-top-0
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render 'layouts/header/read_only_banner'
+ = render "layouts/flash", extra_flash_class: 'limit-container-width'
+
+ %div{ class: "#{container_class} limit-container-width" }
+ .content{ id: "content-body" }
+ .panel.panel-default
+ .panel-heading
+ .title
+ = brand_header_logo
+ - logo_text = brand_header_logo_type
+ - if logo_text.present?
+ %span.logo-text.hidden-xs.prepend-left-8
+ = logo_text
+ - if header_link?(:user_dropdown)
+ .navbar-collapse.collapse
+ %ul.nav.navbar-nav
+ %li.header-user.dropdown
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu-nav.dropdown-menu-align-right
+ = render 'layouts/header/current_user_dropdown'
+ = yield
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
new file mode 100644
index 00000000000..4bee6cb97eb
--- /dev/null
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -0,0 +1,51 @@
+- active_tab = local_assigns.fetch(:active_tab, 'blank')
+- f = local_assigns.fetch(:f)
+
+.project-import.row
+ .col-lg-12
+ .form-group.import-btn-container.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .import-buttons
+ - if gitlab_project_import_enabled?
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn js-import-github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - 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_path, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
+ = icon('git', text: 'Repo by URL')
+ .col-lg-12
+ .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index b66e0559603..5beaa3c6d23 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -57,54 +57,11 @@
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
- .project-import.row
- .col-lg-12
- .form-group.import-btn-container.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
- Import project from
- .import-buttons
- - if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn js-import-github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - 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_path, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
- = icon('git', text: 'Repo by URL')
- .col-lg-12
- .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- %hr
- = render "shared/import_form", f: f
- = render 'new_project_fields', f: f, project_name_id: "import-url-name"
+ = render 'import_project_pane', f: f, active_tab: active_tab
+ - else
+ .nothing-here-block
+ %h4 No import options available
+ %p Contact an administrator to enable options for importing your project.
.save-project-loader.hide
.center
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 8f2142af2ce..81984ee94b0 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title "Pipelines"
- page_title = s_("Pipeline|Run Pipeline")
+- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
= s_("Pipeline|Run Pipeline")
@@ -8,17 +9,26 @@
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
- .col-sm-10
+ .col-sm-12
+ = f.label :ref, s_('Pipeline|Create for')
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block
- = s_("Pipeline|Existing branch name, tag")
+ = s_("Pipeline|Existing branch name or tag")
+
+ .col-sm-12.prepend-top-10.js-ci-variable-list-section
+ %label
+ = s_('Pipeline|Variables')
+ %ul.ci-variable-list
+ = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
+ .help-block
+ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
+
.form-actions
- = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..a9dfd9cc786
--- /dev/null
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -0,0 +1,32 @@
+%h3 Group Runners
+
+.bs-callout.bs-callout-warning
+ GitLab Group Runners can execute code for all the projects in this group.
+ They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}.
+
+ - if @project.group
+ %hr
+ - if @project.group_runners_enabled?
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do
+ Disable group Runners
+ - else
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
+ Enable group Runners
+ &nbsp; for this project
+
+- if !@project.group
+ This project does not belong to a group and can therefore not make use of group Runners.
+
+- elsif @group_runners.empty?
+ This group does not provide any group Runners yet.
+
+ - if can?(current_user, :admin_pipeline, @project.group)
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @project.group.runners_token, type: 'group' }
+ - else
+ Ask your group master to setup a group Runner.
+
+- else
+ %h4.underlined-title Available group Runners : #{@group_runners.count}
+ %ul.bordered-list
+ = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
index f9808f7c990..3f5119d408b 100644
--- a/app/views/projects/runners/_index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -23,3 +23,7 @@
= render 'projects/runners/specific_runners'
.col-sm-6
= render 'projects/runners/shared_runners'
+.row
+ .col-sm-6
+ .col-sm-6
+ = render 'projects/runners/group_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 6376496ee1a..0d2c0536eb5 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -26,7 +26,7 @@
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
= link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- - elsif runner.specific?
+ - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit 'Enable for this project', class: 'btn btn-sm'
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 322152cfaca..f33e7e25b68 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -62,6 +62,6 @@
%td Last contact
%td
- if @runner.contacted_at
- = time_ago_with_tooltip @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
- else
Never
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
new file mode 100644
index 00000000000..c5406696bdd
--- /dev/null
+++ b/app/views/users/terms/index.html.haml
@@ -0,0 +1,13 @@
+- redirect_params = { redirect: @redirect } if @redirect
+
+.panel-content.rendered-terms
+ = markdown_field(@term, :terms)
+.row-content-block.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .pull-right
+ = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
+ = _('Accept terms')
+ - if can?(current_user, :decline_terms, @term)
+ .pull-right
+ = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
+ = _('Decline and sign out')
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index f7f498af840..8d708e15a66 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -63,11 +63,10 @@ module Gitlab
end
def find_project(id)
- # We only care about the import JID so we can refresh it. We also only
- # want the project if it hasn't been marked as failed yet. It's possible
- # the import gets marked as stuck when jobs of the current stage failed
- # somehow.
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 7108b531bc2..68d2c5c4331 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -31,7 +31,10 @@ module Gitlab
end
def find_project(id)
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index fbb14efc525..6fdd7592e74 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -22,7 +22,8 @@ class StuckImportJobsWorker
end
def mark_projects_with_jid_as_failed!
- jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h
+ # TODO: Rollback this change to use SQL through #pluck
+ jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
@@ -42,15 +43,15 @@ class StuckImportJobsWorker
end
def enqueued_projects
- Project.with_import_status(:scheduled, :started)
+ Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
end
def enqueued_projects_with_jid
- enqueued_projects.where.not(import_jid: nil)
+ enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def enqueued_projects_without_jid
- enqueued_projects.where(import_jid: nil)
+ enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def error_message