Commit 49070883 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into zj-object-store-artifacts

parents 8aa22e8b cdf212d5
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
</script> </script>
<template> <template>
<div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys"> <div class="append-bottom-default deploy-keys">
<loading-icon <loading-icon
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
size="2" size="2"
......
...@@ -58,6 +58,7 @@ import UsersSelect from './users_select'; ...@@ -58,6 +58,7 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown'; import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob'; import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -403,6 +404,8 @@ import AuditLogs from './audit_logs'; ...@@ -403,6 +404,8 @@ import AuditLogs from './audit_logs';
// Initialize Protected Tag Settings // Initialize Protected Tag Settings
new ProtectedTagCreate(); new ProtectedTagCreate();
new ProtectedTagEditList(); new ProtectedTagEditList();
// Initialize expandable settings panels
initSettingsPanels();
break; break;
case 'projects:ci_cd:show': case 'projects:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
......
function expandSection($section) {
$section.find('.js-settings-toggle').text('Close');
$section.find('.settings-content').addClass('expanded').off('scroll').scrollTop(0);
}
function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand');
$section.find('.settings-content').removeClass('expanded').on('scroll', () => expandSection($section));
}
function toggleSection($section) {
const $content = $section.find('.settings-content');
$content.removeClass('no-animate');
if ($content.hasClass('expanded')) {
closeSection($section);
} else {
expandSection($section);
}
}
export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
$section.on('click', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll', () => expandSection($section));
});
}
...@@ -117,6 +117,14 @@ ...@@ -117,6 +117,14 @@
} }
} }
.user-settings-pipeline-quota {
margin-top: $gl-padding;
.pipeline-quota {
border-top: none;
}
}
table.pipeline-project-metrics tr td { table.pipeline-project-metrics tr td {
padding: $gl-padding; padding: $gl-padding;
} }
......
@keyframes expandMaxHeight {
0% {
max-height: 0;
}
99% {
max-height: 100vh;
}
100% {
max-height: none;
}
}
@keyframes collapseMaxHeight {
0% {
max-height: 100vh;
}
100% {
max-height: 0;
}
}
.settings {
overflow: hidden;
border-bottom: 1px solid $gray-darker;
&:first-of-type {
margin-top: 10px;
}
}
.settings-header {
position: relative;
padding: 20px 110px 10px 0;
h4 {
margin-top: 0;
}
button {
position: absolute;
top: 20px;
right: 6px;
min-width: 80px;
}
}
.settings-content {
max-height: 1px;
overflow-y: scroll;
margin-right: -20px;
padding-right: 130px;
animation: collapseMaxHeight 300ms ease-out;
&.expanded {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
}
&.no-animate {
animation: none;
}
@media(max-width: $screen-sm-max) {
padding-right: 20px;
}
&::before {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-bottom: 4px;
}
&::after {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-top: 20px;
}
}
.settings-list-icon { .settings-list-icon {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: $settings-icon-size; font-size: $settings-icon-size;
......
class Profiles::PipelineQuotaController < Profiles::ApplicationController
def index
@namespace = current_user.namespace
@projects = @namespace.projects.with_shared_runners_limit_enabled.page(params[:page])
end
end
module Projects
class IssueLinksController < ApplicationController
before_action :authorize_admin_issue_link!, only: [:create, :destroy]
def index
render json: issues
end
def create
create_params = params.slice(:issue_references)
result = IssueLinks::CreateService.new(issue, current_user, create_params).execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
end
def destroy
issue_link = IssueLink.find(params[:id])
return render_403 unless can?(current_user, :admin_issue_link, issue_link.target.project)
IssueLinks::DestroyService.new(issue_link, current_user).execute
render json: { issues: issues }
end
private
def issues
IssueLinks::ListService.new(issue, current_user).execute
end
def authorize_admin_issue_link!
render_403 unless can?(current_user, :admin_issue_link, @project)
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: @project.id)
.execute
.find_by!(iid: params[:issue_id])
end
end
end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# label_name: string # label_name: string
# sort: string # sort: string
# non_archived: boolean # non_archived: boolean
# feature_availability_check: boolean (default: true)
# iids: integer[] # iids: integer[]
# #
class IssuableFinder class IssuableFinder
...@@ -25,11 +26,15 @@ class IssuableFinder ...@@ -25,11 +26,15 @@ class IssuableFinder
ARRAY_PARAMS = { label_name: [], iids: [] }.freeze ARRAY_PARAMS = { label_name: [], iids: [] }.freeze
VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze
DEFAULT_PARAMS = {
feature_availability_check: true
}.freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = DEFAULT_PARAMS.merge(params).with_indifferent_access
end end
def execute def execute
...@@ -126,7 +131,20 @@ class IssuableFinder ...@@ -126,7 +131,20 @@ class IssuableFinder
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # Querying through feature availability for an user is expensive
# (i.e. https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1719#note_31406525),
# and there are cases which a project level access check should be enough.
# In any case, `feature_availability_check` param should be kept with `true`
# by default.
#
projects =
if params[:feature_availability_check]
projects.with_feature_available_for_user(klass, current_user)
else
projects
end
@projects = projects.reorder(nil)
end end
def search def search
......
module EE module EE
module GroupsHelper module NamespaceHelper
def group_shared_runner_limits_quota(group) def namespace_shared_runner_limits_quota(namespace)
used = group.shared_runners_minutes.to_i used = namespace.shared_runners_minutes.to_i
if group.shared_runners_minutes_limit_enabled? if namespace.shared_runners_minutes_limit_enabled?
limit = group.actual_shared_runners_minutes_limit limit = namespace.actual_shared_runners_minutes_limit
status = group.shared_runners_minutes_used? ? 'over_quota' : 'under_quota' status = namespace.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
else else
limit = 'Unlimited' limit = 'Unlimited'
status = 'disabled' status = 'disabled'
...@@ -16,14 +16,14 @@ module EE ...@@ -16,14 +16,14 @@ module EE
end end
end end
def group_shared_runner_limits_percent_used(group) def namespace_shared_runner_limits_percent_used(namespace)
return 0 unless group.shared_runners_minutes_limit_enabled? return 0 unless namespace.shared_runners_minutes_limit_enabled?
100 * group.shared_runners_minutes.to_i / group.actual_shared_runners_minutes_limit 100 * namespace.shared_runners_minutes.to_i / namespace.actual_shared_runners_minutes_limit
end end
def group_shared_runner_limits_progress_bar(group) def namespace_shared_runner_limits_progress_bar(namespace)
percent = [group_shared_runner_limits_percent_used(group), 100].min percent = [namespace_shared_runner_limits_percent_used(namespace), 100].min
status = status =
if percent == 100 if percent == 100
......
...@@ -52,7 +52,8 @@ module Ci ...@@ -52,7 +52,8 @@ module Ci
trigger: 3, trigger: 3,
schedule: 4, schedule: 4,
api: 5, api: 5,
external: 6 external: 6,
pipeline: 7
} }
state_machine :status, initial: :created do state_machine :status, initial: :created do
...@@ -376,7 +377,8 @@ module Ci ...@@ -376,7 +377,8 @@ module Ci
def predefined_variables def predefined_variables
[ [
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true } { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
{ key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
] ]
end end
......
class Geo::BaseRegistry < ActiveRecord::Base class Geo::BaseRegistry < ActiveRecord::Base
self.abstract_class = true self.abstract_class = true
if Gitlab::Geo.configured? && (Gitlab::Geo.secondary? || Rails.env.test?) if Gitlab::Geo.secondary_role_enabled?
establish_connection Rails.configuration.geo_database establish_connection Rails.configuration.geo_database
end end
end end
...@@ -21,9 +21,7 @@ class GeoNode < ActiveRecord::Base ...@@ -21,9 +21,7 @@ class GeoNode < ActiveRecord::Base
validates :encrypted_secret_access_key, presence: true validates :encrypted_secret_access_key, presence: true
after_initialize :build_dependents after_initialize :build_dependents
after_save :refresh_bulk_notify_worker_status
after_save :expire_cache! after_save :expire_cache!
after_destroy :refresh_bulk_notify_worker_status
after_destroy :expire_cache! after_destroy :expire_cache!
before_validation :update_dependents_attributes before_validation :update_dependents_attributes
...@@ -130,10 +128,6 @@ class GeoNode < ActiveRecord::Base ...@@ -130,10 +128,6 @@ class GeoNode < ActiveRecord::Base
{ protocol: schema, host: host, port: port, script_name: relative_url } { protocol: schema, host: host, port: port, script_name: relative_url }
end end
def refresh_bulk_notify_worker_status
Gitlab::Geo.configure_cron_jobs!
end
def build_dependents def build_dependents
unless persisted? unless persisted?
self.build_geo_node_key if geo_node_key.nil? self.build_geo_node_key if geo_node_key.nil?
......
class IssueLink < ActiveRecord::Base
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
...@@ -7,12 +7,14 @@ class License < ActiveRecord::Base ...@@ -7,12 +7,14 @@ class License < ActiveRecord::Base
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
FEATURE_CODES = { FEATURE_CODES = {
geo: GEO_FEATURE, geo: GEO_FEATURE,
auditor_user: AUDITOR_USER_FEATURE, auditor_user: AUDITOR_USER_FEATURE,
service_desk: SERVICE_DESK_FEATURE, service_desk: SERVICE_DESK_FEATURE,
object_storage: OBJECT_STORAGE_FEATURE, object_storage: OBJECT_STORAGE_FEATURE,
related_issues: RELATED_ISSUES_FEATURE,
# Features that make sense to Namespace: # Features that make sense to Namespace:
deploy_board: DEPLOY_BOARD_FEATURE, deploy_board: DEPLOY_BOARD_FEATURE,
file_lock: FILE_LOCK_FEATURE file_lock: FILE_LOCK_FEATURE
...@@ -24,7 +26,7 @@ class License < ActiveRecord::Base ...@@ -24,7 +26,7 @@ class License < ActiveRecord::Base
EARLY_ADOPTER_PLAN = 'early_adopter'.freeze EARLY_ADOPTER_PLAN = 'early_adopter'.freeze
EES_FEATURES = [ EES_FEATURES = [
# .. { RELATED_ISSUES_FEATURE => 1 }
].freeze ].freeze
EEP_FEATURES = [ EEP_FEATURES = [
......
...@@ -3,7 +3,7 @@ class SystemNoteMetadata < ActiveRecord::Base ...@@ -3,7 +3,7 @@ class SystemNoteMetadata < ActiveRecord::Base
commit description merge confidential visible label assignee cross_reference commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved opened closed merged title time_tracking branch milestone discussion task moved opened closed merged
outdated outdated
approved unapproved approved unapproved relate unrelate
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -22,6 +22,11 @@ module EE ...@@ -22,6 +22,11 @@ module EE
cannot! :create_note cannot! :create_note
cannot! :read_project cannot! :read_project
end end
unless project.feature_available?(:related_issues)
cannot! :read_issue_link
cannot! :admin_issue_link
end
end end
end end
end end
...@@ -55,6 +55,9 @@ class ProjectPolicy < BasePolicy ...@@ -55,6 +55,9 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline_schedule can! :read_pipeline_schedule
can! :read_build can! :read_build
end end
# EE-only
can! :read_issue_link
end end
def reporter_access! def reporter_access!
...@@ -79,6 +82,9 @@ class ProjectPolicy < BasePolicy ...@@ -79,6 +82,9 @@ class ProjectPolicy < BasePolicy
if project.feature_available?(:deploy_board) || Rails.env.development? if project.feature_available?(:deploy_board) || Rails.env.development?
can! :read_deploy_board can! :read_deploy_board
end end
# EE-only
can! :admin_issue_link
end end
# Permissions given when an user is team member of a project # Permissions given when an user is team member of a project
...@@ -321,5 +327,8 @@ class ProjectPolicy < BasePolicy ...@@ -321,5 +327,8 @@ class ProjectPolicy < BasePolicy
# NOTE: may be overridden by IssuePolicy # NOTE: may be overridden by IssuePolicy
can! :read_issue can! :read_issue
# EE-only
can! :read_issue_link
end end
end end
...@@ -2,7 +2,7 @@ module Ci ...@@ -2,7 +2,7 @@ module Ci
class CreatePipelineService < BaseService class CreatePipelineService < BaseService
attr_reader :pipeline attr_reader :pipeline
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
source: source, source: source,
project: project, project: project,
...@@ -51,7 +51,7 @@ module Ci ...@@ -51,7 +51,7 @@ module Ci
return error('No builds for this pipeline.') return error('No builds for this pipeline.')
end end
_create_pipeline _create_pipeline(&block)
end end
private private
...@@ -60,6 +60,8 @@ module Ci ...@@ -60,6 +60,8 @@ module Ci
Ci::Pipeline.transaction do Ci::Pipeline.transaction do
update_merge_requests_head_pipeline if pipeline.save update_merge_requests_head_pipeline if pipeline.save
yield(pipeline) if block_given?
Ci::CreatePipelineBuildsService Ci::CreatePipelineBuildsService
.new(project, current_user) .new(project, current_user)
.execute(pipeline) .execute(pipeline)
......
module Ci
class PipelineTriggerService < BaseService
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
elsif job_from_token
create_pipeline_from_job(job_from_token)
end
end
private
def create_pipeline_from_trigger(trigger)
# this check is to not leak the presence of the project if user cannot read it
return unless trigger.project == project
trigger_request = trigger.trigger_requests.create(variables: params[:variables])
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted?
success(pipeline: pipeline)
else
error(pipeline.errors.messages, 400)
end
end
def create_pipeline_from_job(job)
# this check is to not leak the presence of the project if user cannot read it
return unless can?(job.user, :read_project, project)
return error("400 Job has to be running", 400) unless job.running?
return error("400 Variables not supported", 400) if params[:variables].any?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]).
execute(:pipeline, ignore_skip_ci: true) do |pipeline|
job.sourced_pipelines.create!(
source_pipeline: job.pipeline,
source_project: job.project,
pipeline: pipeline,
project: project)
end
if pipeline.persisted?
success(pipeline: pipeline)
else
error(pipeline.errors.messages, 400)
end
end
def trigger_from_token
return @trigger if defined?(@trigger)
@trigger = Ci::Trigger.find_by_token(params[:token].to_s)
end
def job_from_token
return @job if defined?(@job)
@job = Ci::Build.find_by_token(params[:token].to_s)
end
end
end
module IssueLinks
class CreateService < BaseService
def initialize(issue, user, params)
@issue, @current_user, @params = issue, user, params.dup
end
def execute
if referenced_issues.blank?
return error('No Issue found for given reference', 401)
end
create_issue_links
success
end
private
def create_issue_links
referenced_issues.each do |referenced_issue|
create_notes(referenced_issue) if relate_issues(referenced_issue)
end
end
def relate_issues(referenced_issue)
IssueLink.create(source: @issue, target: referenced_issue)
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(@issue, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, @issue, current_user)
end
def referenced_issues
@referenced_issues ||= begin
issue_references = params[:issue_references]
text = issue_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(@issue.project, @current_user)
extractor.analyze(text)
extractor.issues.select do |issue|
can?(current_user, :admin_issue_link, issue)
end
end
end
end
end
module IssueLinks
class DestroyService < BaseService
def initialize(issue_link, user)
@issue_link = issue_link
@current_user = user
@issue = issue_link.source
@referenced_issue = issue_link.target
end
def execute
remove_relation
create_notes
success(message: 'Relation was removed')
end
private
def remove_relation
@issue_link.destroy!
end
def create_notes
SystemNoteService.unrelate_issue(@issue, @referenced_issue, current_user)
SystemNoteService.unrelate_issue(@referenced_issue, @issue, current_user)
end
end
end
module IssueLinks
class ListService
include Gitlab::Routing
def initialize(issue, user)
@issue, @current_user, @project = issue, user, issue.project
end
def execute
issues.map do |referenced_issue|
{
id: referenced_issue.id,
iid: referenced_issue.iid,
title: referenced_issue.title,
state: referenced_issue.state,
project_path: referenced_issue.project.path,
namespace_full_path: referenced_issue.project.namespace.full_path,
path: namespace_project_issue_path(referenced_issue.project.namespace, referenced_issue.project, referenced_issue.iid),
destroy_relation_path: destroy_relation_path(referenced_issue)
}
end
end
private
def issues
related_issues = Issue
.select(['issues.*', 'issue_links.id AS issue_links_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{@issue.id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{@issue.id})")
.preload(project: :namespace)
.reorder('issue_links_id')
Ability.issues_readable_by_user(related_issues, @current_user)
end
def destroy_relation_path(issue)
# Make sure the user can admin both the current issue AND the
# referenced issue projects in order to return the removal link.
if can_destroy_issue_link_on_current_project? && can_destroy_issue_link?(issue.project)
namespace_project_issue_link_path(@project.namespace,
@issue.project,
@issue.iid,
issue.issue_links_id)
end
end
def can_destroy_issue_link_on_current_project?
return @can_destroy_on_current_project if defined?(@can_destroy_on_current_project)
@can_destroy_on_current_project = can_destroy_issue_link?(@project)
end
def can_destroy_issue_link?(project)
Ability.allowed?(@current_user, :admin_issue_link, project)
end
end
end
...@@ -41,6 +41,7 @@ module Issues ...@@ -41,6 +41,7 @@ module Issues
'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) }, 'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) },
'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) }, 'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) },
'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_s(:csv) }, 'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_s(:csv) },
'Closed At (UTC)' => -> (issue) { issue.closed_at&.to_s(:csv) },
'Milestone' => -> (issue) { issue.milestone&.title }, 'Milestone' => -> (issue) { issue.milestone&.title },
'Labels' => -> (issue) { @labels[issue.id].sort.join(',').presence } 'Labels' => -> (issue) { @labels[issue.id].sort.join(',').presence }
} }
......
...@@ -552,6 +552,38 @@ module SystemNoteService ...@@ -552,6 +552,38 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "marked this issue as related to gitlab-ce#9001"
#
# Returns the created Note object
def relate_issue(noteable, noteable_ref, user)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'relate'))
end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "removed the relation with gitlab-ce#9001"
#
# Returns the created Note object
def unrelate_issue(noteable, noteable_ref, user)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate'))
end
# Called when the merge request is approved by user # Called when the merge request is approved by user
# #
# noteable - Noteable object # noteable - Noteable object
......
...@@ -9,44 +9,5 @@ ...@@ -9,44 +9,5 @@
%strong= @group.name %strong= @group.name
group group
.pipeline-quota.container-fluid = render "namespaces/pipelines_quota/list",
.row locals: { namespace: @group, projects: @projects }
.col-sm-6
%strong
- last_reset = @group.shared_runners_seconds_last_reset
- if last_reset
Usage since
= last_reset.strftime('%b %d, %Y')
- else
Current period usage
%div
= group_shared_runner_limits_quota(@group)
minutes
.col-sm-6.right
- if @group.shared_runners_minutes_limit_enabled?
#{group_shared_runner_limits_percent_used(@group)}% used
- else
Unlimited
= group_shared_runner_limits_progress_bar(@group)
%table.table.pipeline-project-metrics
%thead
%tr
%th Project
%th Minutes
%tbody
- @projects.each do |project|
%tr
%td
.avatar-container.s20.hidden-xs
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.name, project
%td
= project.shared_runners_minutes
- if @projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block This group has no projects which use shared runners
= paginate @projects, theme: "gitlab"
...@@ -51,3 +51,7 @@ ...@@ -51,3 +51,7 @@
= link_to audit_log_profile_path, title: 'Authentication log' do = link_to audit_log_profile_path, title: 'Authentication log' do
%span %span
Authentication log Authentication log
= nav_link(path: 'profiles#pipeline_quota') do
= link_to profile_pipeline_quota_path, title: 'Pipeline quota' do
%span
Pipeline quota
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
%li %li
%span.light Pipeline minutes quota: %span.light Pipeline minutes quota:
%strong %strong
= group_shared_runner_limits_quota(namespace) = namespace_shared_runner_limits_quota(namespace)
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank' = link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
- namespace = locals.fetch(:namespace)
- projects = locals.fetch(:projects)
.pipeline-quota.container-fluid
.row
.col-sm-6
%strong
- last_reset = namespace.shared_runners_seconds_last_reset
- if last_reset
Usage since
= last_reset.strftime('%b %d, %Y')
- else
Current period usage
%div
= namespace_shared_runner_limits_quota(namespace)
minutes
.col-sm-6.right
- if namespace.shared_runners_minutes_limit_enabled?
#{namespace_shared_runner_limits_percent_used(namespace)}% used
- else
Unlimited
= namespace_shared_runner_limits_progress_bar(namespace)
%table.table.pipeline-project-metrics
%thead
%tr
%th Project
%th Minutes
%tbody
- projects.each do |project|
%tr
%td
.avatar-container.s20.hidden-xs
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.name, project
%td
= project.shared_runners_minutes
- if projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block This group has no projects which use shared runners
= paginate projects, theme: "gitlab"
- page_title 'Personal pipelines quota'
= render 'profiles/head'
.user-settings-pipeline-quota.row
.profile-settings-sidebar.col-lg-3
%h4
Personal pipelines quota
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
%p.light
Monthly build minutes usage across shared Runners
.col-lg-9
= render "namespaces/pipelines_quota/list",
locals: { namespace: @namespace, projects: @projects }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= form_errors(@deploy_keys.new_key) = form_errors(@deploy_keys.new_key)
.form-group .form-group
= f.label :title, class: "label-light" = f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', autofocus: true, required: true = f.text_field :title, class: 'form-control', required: true
.form-group .form-group
= f.label :key, class: "label-light" = f.label :key, class: "label-light"
= f.text_area :key, class: "form-control", rows: 5, required: true = f.text_area :key, class: "form-control", rows: 5, required: true
......
.row.prepend-top-default - expanded = Rails.env.test?
.col-lg-3.profile-settings-sidebar %section.settings
%h4.prepend-top-0 .settings-header
%h4
Deploy Keys Deploy Keys
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p %p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
.col-lg-9 .settings-content.no-animate{ class: ('expanded' if expanded) }
%h5.prepend-top-0 %h5.prepend-top-0
Create a new deploy key for this project Create a new deploy key for this project
= render @deploy_keys.form_partial_path = render @deploy_keys.form_partial_path
.col-lg-9.col-lg-offset-3
%hr %hr
#js-deploy-keys{ data: { endpoint: namespace_project_deploy_keys_path } } #js-deploy-keys{ data: { endpoint: namespace_project_deploy_keys_path } }
.row - expanded = Rails.env.test?
= form_errors(@project) %section.settings.project-mirror-settings
.row.prepend-top-default.append-bottom-default .settings-header
= form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f| %h4
.col-lg-3 Pull from a remote repository
%h4.prepend-top-0 %button.btn.js-settings-toggle
Pull from a remote repository = expanded ? 'Close' : 'Expand'
%p.light %p
Set up your project to automatically have its branches, tags, and commits Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository every hour. updated from an upstream repository every hour.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank' = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.col-lg-9 .settings-content.no-animate{ class: ('expanded' if expanded) }
%h5.prepend-top-0 = form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f|
%div
= form_errors(@project)
%h5
Set up mirror repository Set up mirror repository
= render "shared/mirror_update_button" = render "shared/mirror_update_button"
- if @project.mirror_last_update_failed? - if @project.mirror_last_update_failed?
...@@ -43,16 +46,22 @@ ...@@ -43,16 +46,22 @@
They need to have at least master access to this project. They need to have at least master access to this project.
- if @project.builds_enabled? - if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f = render "shared/mirror_trigger_builds_setting", f: f
.col-sm-12 = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
%hr
.col-lg-3 %section.settings
%h4.prepend-top-0 .settings-header
Push to a remote repository %h4
%p.light Push to a remote repository
Set up the remote repository that you want to update with the content of the current repository %button.btn.js-settings-toggle
every time someone pushes to it. = expanded ? 'Close' : 'Expand'
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank' %p
.col-lg-9 Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present? - if @remote_mirror.last_error.present?
.panel.panel-danger .panel.panel-danger
...@@ -76,4 +85,3 @@ ...@@ -76,4 +85,3 @@
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions" = render "projects/mirrors/instructions"
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
%hr
- expanded = Rails.env.test?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches') = page_specific_javascript_bundle_tag('protected_branches')
.row.prepend-top-default.append-bottom-default %section.settings
.col-lg-3 .settings-header
%h4.prepend-top-0 %h4
Protected Branches Protected Branches
%p Keep stable branches secure and force developers to use merge requests. %button.btn.js-settings-toggle
%p.prepend-top-20 = expanded ? 'Close' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected branches are designed to: By default, protected branches are designed to:
%ul %ul
%li prevent their creation, if not already created, from everybody except Masters %li prevent their creation, if not already created, from everybody except Masters
%li prevent pushes from everybody except Masters %li prevent pushes from everybody except Masters
%li prevent <strong>anyone</strong> from force pushing to the branch %li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch %li prevent <strong>anyone</strong> from deleting the branch
%p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
.col-lg-9
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project
= render 'projects/protected_branches/create_protected_branch' = render 'projects/protected_branches/create_protected_branch'
......
- expanded = Rails.env.test?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_tags') = page_specific_javascript_bundle_tag('protected_tags')
.row.prepend-top-default.append-bottom-default.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } } %section.settings.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } }
.col-lg-3 .settings-header
%h4.prepend-top-0 %h4
Protected Tags Protected Tags
%p.prepend-top-20 %button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p
Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
By default, protected tags are designed to: By default, protected tags are designed to:
%ul %ul
%li Prevent tag creation by everybody except Masters %li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag %li Prevent <strong>anyone</strong> from deleting the tag
%p.append-bottom-0 Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
.col-lg-9 %p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag' = render 'projects/protected_tags/create_protected_tag'
......
.row.prepend-top-default.append-bottom-default - expanded = Rails.env.test?
.col-lg-3 %section.settings
%h4.prepend-top-0 .settings-header
%h4
Push Rules Push Rules
%p.light %button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p
Push Rules outline what is accepted for this project. Push Rules outline what is accepted for this project.
.col-lg-9 .settings-content.no-animate{ class: ('expanded' if expanded) }
%h5.prepend-top-0 %h5
Add new push rule Add new push rule
= form_for [@project.namespace.becomes(Namespace), @project, @push_rule] do |f| = form_for [@project.namespace.becomes(Namespace), @project, @push_rule] do |f|
= form_errors(@push_rule) = form_errors(@push_rule)
......
- page_title "Repository" - page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/settings/head" = render "projects/settings/head"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('deploy_keys') = page_specific_javascript_bundle_tag('deploy_keys')
= render @deploy_keys
= render "projects/push_rules/index" = render "projects/push_rules/index"
= render "projects/mirrors/show" = render "projects/mirrors/show"
= render "projects/protected_branches/index" = render "projects/protected_branches/index"
= render "projects/protected_tags/index" = render "projects/protected_tags/index"
= render @deploy_keys
...@@ -22,7 +22,7 @@ class GeoFileDownloadDispatchWorker ...@@ -22,7 +22,7 @@ class GeoFileDownloadDispatchWorker
# files, excluding ones in progress. # files, excluding ones in progress.
# 5. Quit when we have scheduled all downloads or exceeded an hour. # 5. Quit when we have scheduled all downloads or exceeded an hour.
def perform def perform
return unless Gitlab::Geo.configured? return unless Gitlab::Geo.secondary_role_enabled?
return unless Gitlab::Geo.secondary? return unless Gitlab::Geo.secondary?
@start_time = Time.now @start_time = Time.now
...@@ -153,7 +153,7 @@ class GeoFileDownloadDispatchWorker ...@@ -153,7 +153,7 @@ class GeoFileDownloadDispatchWorker
def node_enabled? def node_enabled?
# Only check every minute to avoid polling the DB excessively # Only check every minute to avoid polling the DB excessively
unless @last_enabled_check.present? && (Time.now - @last_enabled_check > 1.minute) unless @last_enabled_check.present? && @last_enabled_check > 1.minute.ago
@last_enabled_check = Time.now @last_enabled_check = Time.now
@current_node_enabled = nil @current_node_enabled = nil
end end
......
...@@ -7,7 +7,7 @@ class GeoRepositorySyncWorker ...@@ -7,7 +7,7 @@ class GeoRepositorySyncWorker
LAST_SYNC_INTERVAL = 24.hours LAST_SYNC_INTERVAL = 24.hours
def perform def perform
return unless Gitlab::Geo.configured? return unless Gitlab::Geo.secondary_role_enabled?
return unless Gitlab::Geo.primary_node.present? return unless Gitlab::Geo.primary_node.present?
start_time = Time.now start_time = Time.now
...@@ -20,7 +20,7 @@ class GeoRepositorySyncWorker ...@@ -20,7 +20,7 @@ class GeoRepositorySyncWorker
project_ids.each do |project_id| project_ids.each do |project_id|
begin begin
break if over_time?(start_time) break if over_time?(start_time)
break unless Gitlab::Geo.current_node_enabled? break unless node_enabled?
# We try to obtain a lease here for the entire sync process because we # We try to obtain a lease here for the entire sync process because we
# want to sync the repositories continuously at a controlled rate # want to sync the repositories continuously at a controlled rate
...@@ -73,6 +73,16 @@ class GeoRepositorySyncWorker ...@@ -73,6 +73,16 @@ class GeoRepositorySyncWorker
Time.now - start_time >= RUN_TIME Time.now - start_time >= RUN_TIME
end end
def node_enabled?
# Only check every minute to avoid polling the DB excessively
unless @last_enabled_check.present? && @last_enabled_check > 1.minute.ago
@last_enabled_check = Time.now
@current_node_enabled = nil
end
@current_node_enabled ||= Gitlab::Geo.current_node_enabled?
end
def try_obtain_lease def try_obtain_lease
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout).try_obtain lease = Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout).try_obtain
......
---
title: Allows manually adding bi-directional relationships between issues in the issue page (EES feature)
merge_request:
author:
---
title: Geo - Properly set tracking database connection and cron jobs on secondary nodes
merge_request:
author:
---
title: Allow to Trigger Pipeline using CI Job Token
merge_request:
author:
---
title: Allow to view Personal pipelines quota
merge_request:
author:
---
title: "[Elasticsearch] Improve code search for camel case"
merge_request:
author:
---
title: Add closed_at field to issue CSV export
merge_request:
author:
---
title: Fix incorrect ETag cache key when relative instance URL is used
merge_request: 11964
author:
...@@ -630,6 +630,12 @@ production: &base ...@@ -630,6 +630,12 @@ production: &base
# host: localhost # host: localhost
# port: 3808 # port: 3808
## GitLab Geo settings (EE-only)
geo_primary_role:
enabled: false
geo_secondary_role:
enabled: false
# #
# 5. Extra customization # 5. Extra customization
# ========================== # ==========================
...@@ -713,6 +719,10 @@ test: ...@@ -713,6 +719,10 @@ test:
user_filter: '' user_filter: ''
group_base: 'ou=groups,dc=example,dc=com' group_base: 'ou=groups,dc=example,dc=com'
admin_group: '' admin_group: ''
geo_primary_role:
enabled: true
geo_secondary_role:
enabled: true
staging: staging:
<<: *base <<: *base
...@@ -350,6 +350,10 @@ Settings.pages['external_https'] ||= false unless Settings.pages['external_http ...@@ -350,6 +350,10 @@ Settings.pages['external_https'] ||= false unless Settings.pages['external_http
# Geo # Geo
# #
Settings.gitlab['geo_status_timeout'] ||= 10 Settings.gitlab['geo_status_timeout'] ||= 10
Settings['geo_primary_role'] ||= Settingslogic.new({})
Settings.geo_primary_role['enabled'] = false if Settings.geo_primary_role['enabled'].nil?
Settings['geo_secondary_role'] ||= Settingslogic.new({})
Settings.geo_secondary_role['enabled'] = false if Settings.geo_secondary_role['enabled'].nil?
# #
# Git LFS # Git LFS
......
if File.exist?(Rails.root.join('config/database_geo.yml')) if Gitlab::Geo.secondary_role_enabled?
Rails.application.configure do Rails.application.configure do
config.geo_database = config_for(:database_geo) config.geo_database = config_for(:database_geo)
end end
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
en: en:
hello: "Hello world" hello: "Hello world"
activerecord:
attributes:
issue_link:
source: Source issue
target: Target issue
errors: errors:
messages: messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one." label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
......
...@@ -47,5 +47,9 @@ resource :profile, only: [:show, :update] do ...@@ -47,5 +47,9 @@ resource :profile, only: [:show, :update] do
end end
resources :u2f_registrations, only: [:destroy] resources :u2f_registrations, only: [:destroy]
## EE-specific
resources :pipeline_quota, only: [:index]
## EE-specific
end end
end end
...@@ -311,6 +311,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -311,6 +311,8 @@ constraints(ProjectUrlConstrainer.new) do
post :bulk_update post :bulk_update
post :export_csv post :export_csv
end end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
......
...@@ -31,6 +31,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d ...@@ -31,6 +31,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects get :contributed, as: :contributed_projects
get :snippets get :snippets
get :exists get :exists
get :pipelines_quota
get '/', to: redirect('/%{username}'), as: nil get '/', to: redirect('/%{username}'), as: nil
end end
......
class CreateIssueLinksTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :issue_links do |t|
t.integer :source_id, null: false, index: true
t.integer :target_id, null: false, index: true
t.timestamps null: true
end
add_index :issue_links, [:source_id, :target_id], unique: true
add_concurrent_foreign_key :issue_links, :issues, column: :source_id
add_concurrent_foreign_key :issue_links, :issues, column: :target_id
end
def down
drop_table :issue_links
end
end
...@@ -593,6 +593,17 @@ ActiveRecord::Schema.define(version: 20170602003304) do ...@@ -593,6 +593,17 @@ ActiveRecord::Schema.define(version: 20170602003304) do
add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
create_table "issue_links", force: :cascade do |t|
t.integer "source_id", null: false
t.integer "target_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "issue_links", ["source_id", "target_id"], name: "index_issue_links_on_source_id_and_target_id", unique: true, using: :btree
add_index "issue_links", ["source_id"], name: "index_issue_links_on_source_id", using: :btree
add_index "issue_links", ["target_id"], name: "index_issue_links_on_target_id", using: :btree
create_table "issue_metrics", force: :cascade do |t| create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at" t.datetime "first_mentioned_in_commit_at"
...@@ -1710,6 +1721,8 @@ ActiveRecord::Schema.define(version: 20170602003304) do ...@@ -1710,6 +1721,8 @@ ActiveRecord::Schema.define(version: 20170602003304) do
add_foreign_key "container_repositories", "projects" add_foreign_key "container_repositories", "projects"
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
- [Introduced][ci-229] in GitLab CE 7.14. - [Introduced][ci-229] in GitLab CE 7.14.
- GitLab 8.12 has a completely redesigned job permissions system. Read all - GitLab 8.12 has a completely redesigned job permissions system. Read all
about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers). about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
- GitLab 9.0 introduced a trigger ownership to solve permission problems. - GitLab 9.0 introduced a trigger ownership to solve permission problems,
- GitLab 9.3 introduced an ability to use CI Job Token to trigger dependent pipelines,
Triggers can be used to force a rebuild of a specific `ref` (branch or tag) Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call. with an API call.
...@@ -161,6 +162,25 @@ probably not the wisest idea, so you might want to use a ...@@ -161,6 +162,25 @@ probably not the wisest idea, so you might want to use a
[secure variable](../variables/README.md#user-defined-variables-secure-variables) [secure variable](../variables/README.md#user-defined-variables-secure-variables)
for that purpose._ for that purpose._
---
Since GitLab 9.3 you can trigger a new pipeline using a CI_JOB_TOKEN.
This method currently doesn't support Variables.
The support for them will be included in 9.4 of GitLab.
This way of triggering creates a dependent pipeline relation visible on the Pipeline Graph.
```yaml
build_docs:
stage: deploy
script:
- "curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
only:
- tags
```
Pipelines triggered that way do expose a special variable: `CI_PIPELINE_SOURCE=pipeline`.
### Making use of trigger variables ### Making use of trigger variables
Using trigger variables can be proven useful for a variety of reasons. Using trigger variables can be proven useful for a variety of reasons.
......
...@@ -53,6 +53,7 @@ future GitLab releases.** ...@@ -53,6 +53,7 @@ future GitLab releases.**
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | | **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_SOURCE** | 9.3 | all | The variable indicates how the pipeline was triggered, possible options are: push, web, trigger, schedule, api, pipeline |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
......
...@@ -7,7 +7,7 @@ feature tests with Capybara for e2e (end-to-end) integration testing. ...@@ -7,7 +7,7 @@ feature tests with Capybara for e2e (end-to-end) integration testing.
Unit and feature tests need to be written for all new features. Unit and feature tests need to be written for all new features.
Most of the time, you should use rspec for your feature tests. Most of the time, you should use rspec for your feature tests.
There are cases where the behaviour you are testing is not worth the time spent running the full application, There are cases where the behaviour you are testing is not worth the time spent running the full application,
for example, if you are testing styling, animation or small actions that don't involve the backend, for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend,
you should write an integration test using Jasmine. you should write an integration test using Jasmine.
![Testing priority triangle](img/testing_triangle.png) ![Testing priority triangle](img/testing_triangle.png)
......
...@@ -264,6 +264,22 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ...@@ -264,6 +264,22 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete! If all items are green, then congratulations, the upgrade is complete!
### 13. Elasticsearch index update (if you currently use Elasticsearch)
In 9.3 release we changed the index mapping to improve partial word matching. Please re-create your index by using one of two ways listed below:
1. Re-create the index. The following command is acceptable for not very big GitLab instances (storage size no more than few gigabytes).
```
# Omnibus installations
sudo gitlab-rake gitlab:elastic:index
# Installations from source
bundle exec rake gitlab:elastic:index
```
1. For very big GitLab instances we recommend following [Add GitLab's data to the Elasticsearch index](../integration/elasticsearch.md#add-gitlabs-data-to-the-elasticsearch-index).
## Things went south? Revert to previous version (9.2) ## Things went south? Revert to previous version (9.2)
### 1. Revert the code to the previous version ### 1. Revert the code to the previous version
......
...@@ -35,7 +35,6 @@ module API ...@@ -35,7 +35,6 @@ module API
get 'status' do get 'status' do
authenticate_by_gitlab_geo_node_token! authenticate_by_gitlab_geo_node_token!
require_node_to_be_secondary! require_node_to_be_secondary!
require_node_to_have_tracking_db!
present GeoNodeStatus.new(id: Gitlab::Geo.current_node.id), with: Entities::GeoNodeStatus present GeoNodeStatus.new(id: Gitlab::Geo.current_node.id), with: Entities::GeoNodeStatus
end end
...@@ -110,10 +109,6 @@ module API ...@@ -110,10 +109,6 @@ module API
def require_node_to_be_secondary! def require_node_to_be_secondary!
forbidden! 'Geo node is not secondary node.' unless Gitlab::Geo.current_node&.secondary? forbidden! 'Geo node is not secondary node.' unless Gitlab::Geo.current_node&.secondary?
end end
def require_node_to_have_tracking_db!
not_found! 'Geo node does not have its tracking database enabled.' unless Gitlab::Geo.configured?
end
end end
end end
end end
...@@ -11,28 +11,26 @@ module API ...@@ -11,28 +11,26 @@ module API
end end
params do params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
requires :token, type: String, desc: 'The unique token of trigger' requires :token, type: String, desc: 'The unique token of trigger or job token'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build' optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
# validate variables # validate variables
variables = params[:variables].to_h params[:variables] = params[:variables].to_h
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400) render_api_error!('variables needs to be a map of key-valued strings', 400)
end end
# create request and trigger builds project = find_project(params[:id])
trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) not_found! unless project
if trigger_request
present trigger_request.pipeline, with: Entities::Pipeline result = Ci::PipelineTriggerService.new(project, nil, params).execute
not_found! unless result
if result[:http_status]
render_api_error!(result[:message], result[:http_status])
else else
errors = 'No pipeline created' present result[:pipeline], with: Entities::Pipeline
render_api_error!(errors, 400)
end end
end end
......
...@@ -33,7 +33,7 @@ module Elasticsearch ...@@ -33,7 +33,7 @@ module Elasticsearch
code_analyzer: { code_analyzer: {
type: 'custom', type: 'custom',
tokenizer: 'standard', tokenizer: 'standard',
filter: %w(code lowercase asciifolding), filter: %w(code edgeNGram_filter lowercase asciifolding),
char_filter: ["code_mapping"] char_filter: ["code_mapping"]
}, },
code_search_analyzer: { code_search_analyzer: {
...@@ -61,8 +61,14 @@ module Elasticsearch ...@@ -61,8 +61,14 @@ module Elasticsearch
preserve_original: 1, preserve_original: 1,
patterns: [ patterns: [
"(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)", "(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)",
"(\\d+)" "(\\d+)",
"(?=([\\p{Lu}]+[\\p{L}]+))"
] ]
},
edgeNGram_filter: {
type: 'edgeNGram',
min_gram: 2,
max_gram: 40
} }
}, },
char_filter: { char_filter: {
......
...@@ -6,12 +6,13 @@ module Gitlab ...@@ -6,12 +6,13 @@ module Gitlab
end end
def call(env) def call(env)
route = Gitlab::EtagCaching::Router.match(env) request = Rack::Request.new(env)
route = Gitlab::EtagCaching::Router.match(request)
return @app.call(env) unless route return @app.call(env) unless route
track_event(:etag_caching_middleware_used, route) track_event(:etag_caching_middleware_used, route)
etag, cached_value_present = get_etag(env) etag, cached_value_present = get_etag(request)
if_none_match = env['HTTP_IF_NONE_MATCH'] if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag if if_none_match == etag
...@@ -27,8 +28,8 @@ module Gitlab ...@@ -27,8 +28,8 @@ module Gitlab
private private
def get_etag(env) def get_etag(request)
cache_key = env['PATH_INFO'] cache_key = request.path
store = Gitlab::EtagCaching::Store.new store = Gitlab::EtagCaching::Store.new
current_value = store.get(cache_key) current_value = store.get(cache_key)
cached_value_present = current_value.present? cached_value_present = current_value.present?
......
...@@ -53,8 +53,8 @@ module Gitlab ...@@ -53,8 +53,8 @@ module Gitlab
) )
].freeze ].freeze
def self.match(env) def self.match(request)
ROUTES.find { |route| route.regexp.match(env['PATH_INFO']) } ROUTES.find { |route| route.regexp.match(request.path_info) }
end end
end end
end end
......
...@@ -42,8 +42,12 @@ module Gitlab ...@@ -42,8 +42,12 @@ module Gitlab
Gitlab::Geo.current_node.reload.enabled? Gitlab::Geo.current_node.reload.enabled?
end end
def self.configured? def self.primary_role_enabled?
Rails.configuration.respond_to?(:geo_database) Gitlab.config.geo_primary_role['enabled']
end
def self.secondary_role_enabled?
Gitlab.config.geo_secondary_role['enabled']
end end
def self.license_allows? def self.license_allows?
...@@ -94,9 +98,9 @@ module Gitlab ...@@ -94,9 +98,9 @@ module Gitlab
end end
def self.configure_cron_jobs! def self.configure_cron_jobs!
if self.primary? if self.primary_role_enabled?
self.configure_primary_jobs! self.configure_primary_jobs!
elsif self.secondary? elsif self.secondary_role_enabled?
self.configure_secondary_jobs! self.configure_secondary_jobs!
else else
self.disable_all_jobs! self.disable_all_jobs!
......
...@@ -3,7 +3,8 @@ module Gitlab ...@@ -3,7 +3,8 @@ module Gitlab
class HealthCheck class HealthCheck
def self.perform_checks def self.perform_checks
return '' unless Gitlab::Geo.secondary? return '' unless Gitlab::Geo.secondary?
return 'The Geo database configuration file is missing.' unless Gitlab::Geo.configured? return 'The Geo secondary role is disabled.' unless Gitlab::Geo.secondary_role_enabled?
return 'The Geo database configuration file is missing.' unless self.geo_database_configured?
return 'The Geo node has a database that is not configured for streaming replication with the primary node.' unless self.database_secondary? return 'The Geo node has a database that is not configured for streaming replication with the primary node.' unless self.database_secondary?
database_version = self.get_database_version.to_i database_version = self.get_database_version.to_i
...@@ -57,6 +58,10 @@ module Gitlab ...@@ -57,6 +58,10 @@ module Gitlab
.first .first
.fetch('pg_is_in_recovery') == 't' .fetch('pg_is_in_recovery') == 't'
end end
def self.geo_database_configured?
Rails.configuration.respond_to?(:geo_database)
end
end end
end end
end end
...@@ -284,13 +284,18 @@ describe Projects::MergeRequestsController do ...@@ -284,13 +284,18 @@ describe Projects::MergeRequestsController do
context 'number of queries' do context 'number of queries' do
it 'verifies number of queries' do it 'verifies number of queries' do
RequestStore.begin!
# pre-create objects # pre-create objects
merge_request merge_request
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
expect(recorded.count).to be_within(10).of(100) expect(recorded.count).to be_within(1).of(31)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
RequestStore.end!
RequestStore.clear!
end end
end end
end end
......
FactoryGirl.define do
factory :issue_link do
source factory: :issue
target factory: :issue
end
end
require 'spec_helper'
feature 'Profile > Pipeline Quota', feature: true do
let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) }
let!(:project) { create(:empty_project, namespace: namespace, shared_runners_enabled: true) }
before do
login_with(user)
end
it 'is linked within the profile page' do
visit profile_path
page.within('.layout-nav') do
expect(page).to have_selector(:link_or_button, 'Pipeline quota')
end
end
context 'with no quota' do
let(:namespace) { create(:namespace, :with_build_minutes, owner: user) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("400 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'with no projects using shared runners' do
let(:namespace) { create(:namespace, :with_not_used_build_minutes_limit, owner: user) }
let!(:project) { create(:empty_project, namespace: namespace, shared_runners_enabled: false) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("300 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content('This group has no projects which use shared runners')
end
end
end
context 'minutes under quota' do
let(:namespace) { create(:namespace, :with_not_used_build_minutes_limit, owner: user) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("300 / 500 minutes")
expect(page).to have_content("60% used")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'minutes over quota' do
let(:namespace) { create(:namespace, :with_used_build_minutes_limit, owner: user) }
let!(:other_project) { create(:empty_project, namespace: namespace, shared_runners_enabled: false) }
it 'shows correct group quota and projects info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("1000 / 500 minutes")
expect(page).to have_content("200% used")
expect(page).to have_selector('.progress-bar-danger')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content(project.name)
expect(page).not_to have_content(other_project.name)
end
end
end
end
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Project settings > [EE] repository', feature: true do describe 'Project settings > [EE] repository', feature: true do
include Select2Helper include Select2Helper
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) } let(:project) { create(:project_empty_repo) }
...@@ -41,7 +41,7 @@ describe 'Project settings > [EE] repository', feature: true do ...@@ -41,7 +41,7 @@ describe 'Project settings > [EE] repository', feature: true do
end end
it 'sets mirror user' do it 'sets mirror user' do
page.within('.edit_project') do page.within('.project-mirror-settings') do
select2(user2.id, from: '#project_mirror_user_id') select2(user2.id, from: '#project_mirror_user_id')
click_button('Save changes') click_button('Save changes')
......
...@@ -288,6 +288,18 @@ describe IssuesFinder do ...@@ -288,6 +288,18 @@ describe IssuesFinder do
expect(issues.count).to eq 0 expect(issues.count).to eq 0
end end
it 'returns disabled issues if feature_availability_check param set to false' do
[project1, project2].each do |project|
project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
end
issues = described_class
.new(search_user, params.reverse_merge(scope: scope, state: 'opened', feature_availability_check: false))
.execute
expect(issues.count).to eq 3
end
end end
end end
......
...@@ -378,8 +378,8 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -378,8 +378,8 @@ describe Gitlab::Elastic::SearchResults, lib: true do
results = described_class.new(user, 'def', limit_project_ids) results = described_class.new(user, 'def', limit_project_ids)
blobs = results.objects('blobs') blobs = results.objects('blobs')
expect(blobs.first["_source"]["blob"]["content"]).to include("def") expect(blobs.first['_source']['blob']['content']).to include('def')
expect(results.blobs_count).to eq 5 expect(results.blobs_count).to eq 7
end end
it 'finds blobs from public projects only' do it 'finds blobs from public projects only' do
...@@ -388,10 +388,11 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -388,10 +388,11 @@ describe Gitlab::Elastic::SearchResults, lib: true do
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
results = described_class.new(user, 'def', [project_1.id]) results = described_class.new(user, 'def', [project_1.id])
expect(results.blobs_count).to eq 5 expect(results.blobs_count).to eq 7
results = described_class.new(user, 'def', [project_1.id, project_2.id]) results = described_class.new(user, 'def', [project_1.id, project_2.id])
expect(results.blobs_count).to eq 10
expect(results.blobs_count).to eq 14
end end
it 'returns zero when blobs are not found' do it 'returns zero when blobs are not found' do
...@@ -399,6 +400,45 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -399,6 +400,45 @@ describe Gitlab::Elastic::SearchResults, lib: true do
expect(results.blobs_count).to eq 0 expect(results.blobs_count).to eq 0
end end
context 'Searches CamelCased methods' do
before do
project_1.repository.create_file(
user,
'test.txt',
' function writeStringToFile(){} ',
message: 'added test file',
branch_name: 'master')
project_1.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index
end
def search_for(term)
blobs = described_class.new(user, term, [project_1.id]).objects('blobs')
blobs.map do |blob|
blob['_source']['blob']['path']
end
end
it 'find by first word' do
expect(search_for('write')).to include('test.txt')
end
it 'find by first two words' do
expect(search_for('writeString')).to include('test.txt')
end
it 'find by last two words' do
expect(search_for('ToFile')).to include('test.txt')
end
it 'find by exact match' do
expect(search_for('writeStringToFile')).to include('test.txt')
end
end
end end
describe 'Wikis' do describe 'Wikis' do
...@@ -415,7 +455,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -415,7 +455,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
it 'finds wiki blobs' do it 'finds wiki blobs' do
blobs = results.objects('wiki_blobs') blobs = results.objects('wiki_blobs')
expect(blobs.first["_source"]["blob"]["content"]).to include("term") expect(blobs.first['_source']['blob']['content']).to include("term")
expect(results.wiki_blobs_count).to eq 1 expect(results.wiki_blobs_count).to eq 1
end end
...@@ -423,7 +463,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -423,7 +463,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
project_1.add_guest(user) project_1.add_guest(user)
blobs = results.objects('wiki_blobs') blobs = results.objects('wiki_blobs')
expect(blobs.first["_source"]["blob"]["content"]).to include("term") expect(blobs.first['_source']['blob']['content']).to include("term")
expect(results.wiki_blobs_count).to eq 1 expect(results.wiki_blobs_count).to eq 1
end end
......
...@@ -164,6 +164,25 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -164,6 +164,25 @@ describe Gitlab::EtagCaching::Middleware do
end end
end end
context 'when GitLab instance is using a relative URL' do
before do
mock_app_response
end
it 'uses full path as cache key' do
env = {
'PATH_INFO' => enabled_path,
'SCRIPT_NAME' => '/relative-gitlab'
}
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:get).with("/relative-gitlab#{enabled_path}")
.and_return(nil)
middleware.call(env)
end
end
def mock_app_response def mock_app_response
allow(app).to receive(:call).and_return([app_status_code, {}, ['body']]) allow(app).to receive(:call).and_return([app_status_code, {}, ['body']])
end end
......
...@@ -2,115 +2,115 @@ require 'spec_helper' ...@@ -2,115 +2,115 @@ require 'spec_helper'
describe Gitlab::EtagCaching::Router do describe Gitlab::EtagCaching::Router do
it 'matches issue notes endpoint' do it 'matches issue notes endpoint' do
env = build_env( request = build_request(
'/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'issue_notes' expect(result.name).to eq 'issue_notes'
end end
it 'matches issue title endpoint' do it 'matches issue title endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/issues/123/realtime_changes' '/my-group/my-project/issues/123/realtime_changes'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'issue_title' expect(result.name).to eq 'issue_title'
end end
it 'matches project pipelines endpoint' do it 'matches project pipelines endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/pipelines.json' '/my-group/my-project/pipelines.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'project_pipelines' expect(result.name).to eq 'project_pipelines'
end end
it 'matches commit pipelines endpoint' do it 'matches commit pipelines endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'commit_pipelines' expect(result.name).to eq 'commit_pipelines'
end end
it 'matches new merge request pipelines endpoint' do it 'matches new merge request pipelines endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/merge_requests/new.json' '/my-group/my-project/merge_requests/new.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'new_merge_request_pipelines' expect(result.name).to eq 'new_merge_request_pipelines'
end end
it 'matches merge request pipelines endpoint' do it 'matches merge request pipelines endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/merge_requests/234/pipelines.json' '/my-group/my-project/merge_requests/234/pipelines.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'merge_request_pipelines' expect(result.name).to eq 'merge_request_pipelines'
end end
it 'matches build endpoint' do it 'matches build endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/builds/234.json' '/my-group/my-project/builds/234.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'project_build' expect(result.name).to eq 'project_build'
end end
it 'does not match blob with confusing name' do it 'does not match blob with confusing name' do
env = build_env( request = build_request(
'/my-group/my-project/blob/master/pipelines.json' '/my-group/my-project/blob/master/pipelines.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_blank expect(result).to be_blank
end end
it 'matches the environments path' do it 'matches the environments path' do
env = build_env( request = build_request(
'/my-group/my-project/environments.json' '/my-group/my-project/environments.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'environments' expect(result.name).to eq 'environments'
end end
it 'matches pipeline#show endpoint' do it 'matches pipeline#show endpoint' do
env = build_env( request = build_request(
'/my-group/my-project/pipelines/2.json' '/my-group/my-project/pipelines/2.json'
) )
result = described_class.match(env) result = described_class.match(request)
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'project_pipeline' expect(result.name).to eq 'project_pipeline'
end end
def build_env(path) def build_request(path)
{ 'PATH_INFO' => path } double(path_info: path)
end end
end end
...@@ -16,6 +16,13 @@ describe Gitlab::Geo::HealthCheck do ...@@ -16,6 +16,13 @@ describe Gitlab::Geo::HealthCheck do
expect(subject.perform_checks).to be_blank expect(subject.perform_checks).to be_blank
end end
it 'returns an error when secondary role is disabled' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Geo).to receive(:secondary_role_enabled?).and_return(false)
expect(subject.perform_checks).not_to be_blank
end
it 'returns an error when database is not configured for streaming replication' do it 'returns an error when database is not configured for streaming replication' do
allow(Gitlab::Geo).to receive(:secondary?) { true } allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:postgresql?) { true } allow(Gitlab::Database).to receive(:postgresql?) { true }
......
...@@ -127,7 +127,9 @@ describe Gitlab::Geo, lib: true do ...@@ -127,7 +127,9 @@ describe Gitlab::Geo, lib: true do
end end
it 'activates cron jobs for primary' do it 'activates cron jobs for primary' do
allow(described_class).to receive(:primary?).and_return(true) allow(described_class).to receive(:primary_role_enabled?).and_return(true)
allow(described_class).to receive(:secondary_role_enabled?).and_return(false)
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).to be_enabled expect(described_class.bulk_notify_job).to be_enabled
...@@ -136,7 +138,9 @@ describe Gitlab::Geo, lib: true do ...@@ -136,7 +138,9 @@ describe Gitlab::Geo, lib: true do
end end
it 'activates cron jobs for secondary' do it 'activates cron jobs for secondary' do
allow(described_class).to receive(:secondary?).and_return(true) allow(described_class).to receive(:primary_role_enabled?).and_return(false)
allow(described_class).to receive(:secondary_role_enabled?).and_return(true)
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled expect(described_class.bulk_notify_job).not_to be_enabled
...@@ -145,6 +149,9 @@ describe Gitlab::Geo, lib: true do ...@@ -145,6 +149,9 @@ describe Gitlab::Geo, lib: true do
end end
it 'deactivates all jobs when Geo is not active' do it 'deactivates all jobs when Geo is not active' do
allow(described_class).to receive(:primary_role_enabled?).and_return(false)
allow(described_class).to receive(:secondary_role_enabled?).and_return(false)
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled expect(described_class.bulk_notify_job).not_to be_enabled
......
require 'spec_helper'
describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
context 'self relation' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
end
end
...@@ -10,10 +10,14 @@ describe ProjectPolicy, models: true do ...@@ -10,10 +10,14 @@ describe ProjectPolicy, models: true do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
before do
allow_any_instance_of(License).to receive(:feature_available?) { true }
end
let(:guest_permissions) do let(:guest_permissions) do
%i[ %i[
read_project read_board read_list read_wiki read_issue read_label read_project read_board read_list read_wiki read_issue read_label
read_milestone read_project_snippet read_project_member read_issue_link read_milestone read_project_snippet read_project_member
read_note create_project create_issue create_note read_note create_project create_issue create_note
upload_file upload_file
] ]
...@@ -22,7 +26,7 @@ describe ProjectPolicy, models: true do ...@@ -22,7 +26,7 @@ describe ProjectPolicy, models: true do
let(:reporter_permissions) do let(:reporter_permissions) do
%i[ %i[
download_code fork_project create_project_snippet update_issue download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build admin_issue admin_label admin_issue_link admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_merge_request download_wiki_code
] ]
...@@ -71,7 +75,7 @@ describe ProjectPolicy, models: true do ...@@ -71,7 +75,7 @@ describe ProjectPolicy, models: true do
let(:auditor_permissions) do let(:auditor_permissions) do
%i[ %i[
download_code download_wiki_code read_project read_board read_list download_code download_wiki_code read_project read_board read_list
read_wiki read_issue read_label read_milestone read_project_snippet read_wiki read_issue read_label read_issue_link read_milestone read_project_snippet
read_project_member read_note read_cycle_analytics read_pipeline read_project_member read_note read_cycle_analytics read_pipeline
read_build read_commit_status read_container_image read_environment read_build read_commit_status read_container_image read_environment
read_deployment read_merge_request read_pages read_deployment read_merge_request read_pages
......
...@@ -307,14 +307,6 @@ describe API::Geo, api: true do ...@@ -307,14 +307,6 @@ describe API::Geo, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('geo_node_status') expect(response).to match_response_schema('geo_node_status')
end end
it 'responds with a 404 when the tracking database is disabled' do
allow(Gitlab::Geo).to receive(:configured?).and_return(false)
get api('/geo/status'), nil, request.headers
expect(response).to have_http_status(404)
end
end end
context 'when requesting primary node with valid auth header' do context 'when requesting primary node with valid auth header' do
......
...@@ -36,12 +36,6 @@ describe API::Triggers do ...@@ -36,12 +36,6 @@ describe API::Triggers do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'returns unauthorized if token is for different project' do
post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master')
expect(response).to have_http_status(401)
end
end end
context 'Have a commit' do context 'Have a commit' do
...@@ -61,7 +55,7 @@ describe API::Triggers do ...@@ -61,7 +55,7 @@ describe API::Triggers do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No pipeline created') expect(json_response['message']).to eq('base' => ["Reference not found"])
end end
context 'Validates variables' do context 'Validates variables' do
...@@ -93,6 +87,12 @@ describe API::Triggers do ...@@ -93,6 +87,12 @@ describe API::Triggers do
end end
context 'when triggering a pipeline from a trigger token' do context 'when triggering a pipeline from a trigger token' do
it 'does not leak the presence of project when token is for different project' do
post api("/projects/#{project2.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
expect(response).to have_http_status(404)
end
it 'creates builds from the ref given in the URL, not in the body' do it 'creates builds from the ref given in the URL, not in the body' do
expect do expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
...@@ -113,6 +113,93 @@ describe API::Triggers do ...@@ -113,6 +113,93 @@ describe API::Triggers do
end end
end end
end end
context 'when triggering a pipeline from a job token' do
let(:other_job) { create(:ci_build, :running, user: other_user) }
let(:params) { { ref: 'refs/heads/other-branch' } }
subject do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{other_job.token}"), params
end
context 'without user' do
let(:other_user) { nil }
it 'does not leak the presence of project when using valid token' do
subject
expect(response).to have_http_status(404)
end
end
context 'for unreleated user' do
let(:other_user) { create(:user) }
it 'does not leak the presence of project when using valid token' do
subject
expect(response).to have_http_status(404)
end
end
context 'for related user' do
let(:other_user) { create(:user) }
context 'with reporter permissions' do
before do
project.add_reporter(other_user)
end
it 'forbids to create a pipeline' do
subject
expect(response).to have_http_status(400)
expect(json_response['message']).to eq("base" => ["Insufficient permissions to create a new pipeline"])
end
end
context 'with developer permissions' do
before do
project.add_developer(other_user)
end
it 'creates a new pipeline' do
expect { subject }.to change(Ci::Pipeline, :count)
expect(response).to have_http_status(201)
expect(Ci::Pipeline.last.source).to eq('pipeline')
expect(Ci::Pipeline.last.triggered_by_pipeline).not_to be_nil
end
context 'when build is complete' do
before do
other_job.success
end
it 'does not create a pipeline' do
subject
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 Job has to be running')
end
end
context 'when variables are defined' do
let(:params) do
{ ref: 'refs/heads/other-branch',
variables: { 'KEY' => 'VALUE' } }
end
it 'forbids to create a pipeline' do
subject
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 Variables not supported')
end
end
end
end
end
end end
describe 'GET /projects/:id/triggers' do describe 'GET /projects/:id/triggers' do
......
require 'rails_helper'
describe Projects::IssueLinksController do
let(:user) { create :user }
let(:project) { create(:project_empty_repo) }
let(:issue) { create :issue, project: project }
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
end
describe 'GET /*namespace_id/:project_id/issues/:issue_id/links' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) { create :issue_link, source: issue, target: issue_b }
before do
project.team << [user, :guest]
login_as user
end
it 'returns JSON response' do
list_service_response = IssueLinks::ListService.new(issue, user).execute
get namespace_project_issue_links_path(issue_links_params)
expect(response).to have_http_status(200)
expect(json_response).to eq(list_service_response.as_json)
end
end
describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do
let(:issue_b) { create :issue, project: project }
before do
project.team << [user, user_role]
login_as user
end
context 'with success' do
let(:user_role) { :developer }
let(:issue_references) { [issue_b.to_reference] }
it 'returns success JSON' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(response).to have_http_status(200)
expect(json_response).to eq('message' => nil,
'issues' => list_service_response.as_json)
end
end
context 'with failure' do
context 'when unauthorized' do
let(:user_role) { :guest }
let(:issue_references) { [issue_b.to_reference] }
it 'returns 403' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
expect(response).to have_http_status(403)
end
end
context 'when failing service result' do
let(:user_role) { :developer }
let(:issue_references) { ['#999'] }
it 'returns failure JSON' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(response).to have_http_status(401)
expect(json_response).to eq('message' => 'No Issue found for given reference', 'issues' => list_service_response.as_json)
end
end
end
end
describe 'DELETE /*namespace_id/:project_id/issues/:issue_id/link/:id' do
let(:issue_link) { create :issue_link, target: referenced_issue }
before do
project.team << [user, user_role]
login_as user
end
context 'when unauthorized' do
context 'when no authorization on current project' do
let(:referenced_issue) { create :issue, project: project }
let(:user_role) { :guest }
it 'returns 403' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
expect(response).to have_http_status(403)
end
end
context 'when no authorization on the related issue project' do
# unauthorized project issue
let(:referenced_issue) { create :issue }
let(:user_role) { :developer }
it 'returns 403' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
expect(response).to have_http_status(403)
end
end
end
context 'when authorized' do
let(:referenced_issue) { create :issue, project: project }
let(:user_role) { :developer }
it 'returns success JSON' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(json_response).to eq('issues' => list_service_response.as_json)
end
end
end
def issue_links_params(opts = {})
opts.reverse_merge(namespace_id: issue.project.namespace,
project_id: issue.project,
issue_id: issue,
format: :json)
end
end
require 'spec_helper'
describe IssueLinks::CreateService, service: true do
describe '#execute' do
let(:namespace) { create :namespace }
let(:project) { create :empty_project, namespace: namespace }
let(:issue) { create :issue, project: project }
let(:user) { create :user }
let(:params) do
{}
end
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
project.team << [user, :developer]
end
subject { described_class.new(issue, user, params).execute }
context 'when the reference list is empty' do
let(:params) do
{ issue_references: [] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
end
end
context 'when Issue not found' do
let(:params) do
{ issue_references: ['#999'] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
context 'when user has no permission to target project Issue' do
let(:target_issue) { create :issue }
let(:params) do
{ issue_references: [target_issue.to_reference(project)] }
end
it 'returns error' do
target_issue.project.add_guest(user)
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
context 'when there is an issue to relate' do
let(:issue_a) { create :issue, project: project }
let(:another_project) { create :empty_project, namespace: project.namespace }
let(:another_project_issue) { create :issue, project: another_project }
let(:issue_a_ref) { issue_a.to_reference }
let(:another_project_issue_ref) { another_project_issue.to_reference(project) }
let(:params) do
{ issue_references: [issue_a_ref, another_project_issue_ref] }
end
before do
another_project.team << [user, :developer]
end
it 'creates relationships' do
expect { subject }.to change(IssueLink, :count).from(0).to(2)
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue)
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue)
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
it 'creates notes' do
# First two-way relation notes
expect(SystemNoteService).to receive(:relate_issue)
.with(issue, issue_a, user)
expect(SystemNoteService).to receive(:relate_issue)
.with(issue_a, issue, user)
# Second two-way relation notes
expect(SystemNoteService).to receive(:relate_issue)
.with(issue, another_project_issue, user)
expect(SystemNoteService).to receive(:relate_issue)
.with(another_project_issue, issue, user)
subject
end
end
context 'when reference of any already related issue is present' do
let(:issue_a) { create :issue, project: project }
let(:issue_b) { create :issue, project: project }
before do
create :issue_link, source: issue, target: issue_a
end
let(:params) do
{ issue_references: [issue_b.to_reference, issue_a.to_reference] }
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
it 'valid relations are created' do
expect { subject }.to change(IssueLink, :count).from(1).to(2)
expect(IssueLink.find_by!(target: issue_b)).to have_attributes(source: issue)
end
end
end
end
require 'spec_helper'
describe IssueLinks::DestroyService, service: true do
describe '#execute' do
let(:user) { create :user }
let!(:issue_link) { create :issue_link }
subject { described_class.new(issue_link, user).execute }
it 'removes related issue' do
expect { subject }.to change(IssueLink, :count).from(1).to(0)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.source, issue_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.target, issue_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
end
end
require 'spec_helper'
describe IssueLinks::ListService, service: true do
let(:user) { create :user }
let(:project) { create(:project_empty_repo, :private) }
let(:issue) { create :issue, project: project }
let(:user_role) { :developer }
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
project.team << [user, user_role]
end
describe '#execute' do
subject { described_class.new(issue, user).execute }
context 'user can see all issues' do
let(:issue_b) { create :issue, project: project }
let(:issue_c) { create :issue, project: project }
let(:issue_d) { create :issue, project: project }
let!(:issue_link_c) do
create(:issue_link, source: issue_d,
target: issue)
end
let!(:issue_link_b) do
create(:issue_link, source: issue,
target: issue_c)
end
let!(:issue_link_a) do
create(:issue_link, source: issue,
target: issue_b)
end
it 'ensures no N+1 queries are made' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
project = create :empty_project, :public
issue_x = create :issue, project: project
issue_y = create :issue, project: project
issue_z = create :issue, project: project
create :issue_link, source: issue_x, target: issue_y
create :issue_link, source: issue_x, target: issue_z
create :issue_link, source: issue_y, target: issue_z
expect { subject }.not_to exceed_query_limit(control_count)
end
it 'returns related issues JSON' do
expect(subject.size).to eq(3)
expect(subject).to include(include(id: issue_b.id,
iid: issue_b.iid,
title: issue_b.title,
state: issue_b.state,
path: "/#{project.full_path}/issues/#{issue_b.iid}",
project_path: issue_b.project.path,
namespace_full_path: issue_b.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_a.id}"))
expect(subject).to include(include(id: issue_c.id,
iid: issue_c.iid,
title: issue_c.title,
state: issue_c.state,
path: "/#{project.full_path}/issues/#{issue_c.iid}",
project_path: issue_c.project.path,
namespace_full_path: issue_c.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_b.id}"))
expect(subject).to include(include(id: issue_d.id,
iid: issue_d.iid,
title: issue_d.title,
state: issue_d.state,
path: "/#{project.full_path}/issues/#{issue_d.iid}",
project_path: issue_d.project.path,
namespace_full_path: issue_d.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_c.id}"))
end
end
context 'referencing a public project issue' do
let(:public_project) { create :empty_project, :public }
let(:issue_b) { create :issue, project: public_project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'presents issue' do
expect(subject.size).to eq(1)
end
end
context 'referencing issue with removed relationships' do
context 'when referenced a deleted issue' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
issue_b.destroy!
is_expected.to eq([])
end
end
context 'when referenced an issue with deleted project' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
project.destroy!
is_expected.to eq([])
end
end
context 'when referenced an issue with deleted namespace' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
project.namespace.destroy!
is_expected.to eq([])
end
end
end
context 'user cannot see relations' do
context 'when user cannot see the referenced issue' do
let!(:issue_link) do
create(:issue_link, source: issue)
end
it 'returns an empty list' do
is_expected.to eq([])
end
end
context 'when user cannot see the issue that referenced' do
let!(:issue_link) do
create(:issue_link, target: issue)
end
it 'returns an empty list' do
is_expected.to eq([])
end
end
end
context 'remove relations' do
let!(:issue_link) do
create(:issue_link, source: issue, target: referenced_issue)
end
context 'user can admin related issues just on target project' do
let(:user_role) { :guest }
let(:target_project) { create :empty_project }
let(:referenced_issue) { create :issue, project: target_project }
it 'returns no destroy relation path' do
target_project.add_developer(user)
expect(subject.first[:destroy_relation_path]).to be_nil
end
end
context 'user can admin related issues just on source project' do
let(:user_role) { :developer }
let(:target_project) { create :empty_project }
let(:referenced_issue) { create :issue, project: target_project }
it 'returns no destroy relation path' do
target_project.add_guest(user)
expect(subject.first[:destroy_relation_path]).to be_nil
end
end
context 'when user can admin related issues on both projects' do
let(:referenced_issue) { create :issue, project: project }
it 'returns related issue destroy relation path' do
expect(subject.first[:destroy_relation_path])
.to eq("/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link.id}")
end
end
end
end
end
...@@ -39,6 +39,7 @@ describe Issues::ExportCsvService, services: true do ...@@ -39,6 +39,7 @@ describe Issues::ExportCsvService, services: true do
due_date: DateTime.new(2014, 3, 2), due_date: DateTime.new(2014, 3, 2),
created_at: DateTime.new(2015, 4, 3, 2, 1, 0), created_at: DateTime.new(2015, 4, 3, 2, 1, 0),
updated_at: DateTime.new(2016, 5, 4, 3, 2, 1), updated_at: DateTime.new(2016, 5, 4, 3, 2, 1),
closed_at: DateTime.new(2017, 6, 5, 4, 3, 2),
labels: [feature_label, idea_label]) labels: [feature_label, idea_label])
end end
...@@ -101,6 +102,10 @@ describe Issues::ExportCsvService, services: true do ...@@ -101,6 +102,10 @@ describe Issues::ExportCsvService, services: true do
specify 'updated_at' do specify 'updated_at' do
expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01' expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
end end
specify 'closed_at' do
expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
end
end end
context 'with minimal details' do context 'with minimal details' do
......
...@@ -899,6 +899,38 @@ describe SystemNoteService, services: true do ...@@ -899,6 +899,38 @@ describe SystemNoteService, services: true do
end end
end end
describe '.relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { described_class.relate_issue(noteable, noteable_ref, author) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
end
context 'when issue marks another as related' do
it 'sets the note text' do
expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
end
end
end
describe '.unrelate_issue' do
let(:noteable_ref) { create(:issue) }
subject { described_class.unrelate_issue(noteable, noteable_ref, author) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }
end
context 'when issue relation is removed' do
it 'sets the note text' do
expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
end
end
end
describe '.approve_mr' do describe '.approve_mr' do
let(:noteable) { create(:merge_request, source_project: project) } let(:noteable) { create(:merge_request, source_project: project) }
subject { described_class.approve_mr(noteable, author) } subject { described_class.approve_mr(noteable, author) }
......
...@@ -13,10 +13,10 @@ describe GeoFileDownloadDispatchWorker do ...@@ -13,10 +13,10 @@ describe GeoFileDownloadDispatchWorker do
subject { described_class.new } subject { described_class.new }
describe '#perform' do describe '#perform' do
it 'does not schedule anything when tracking DB is not available' do it 'does not schedule anything when secondary role is disabled' do
create(:lfs_object, :with_file) create(:lfs_object, :with_file)
allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false } allow(Gitlab::Geo).to receive(:secondary_role_enabled?) { false }
expect(GeoFileDownloadWorker).not_to receive(:perform_async) expect(GeoFileDownloadWorker).not_to receive(:perform_async)
......
...@@ -52,8 +52,8 @@ describe GeoRepositorySyncWorker do ...@@ -52,8 +52,8 @@ describe GeoRepositorySyncWorker do
subject.perform subject.perform
end end
it 'does not perform Geo::RepositorySyncService when tracking DB is not available' do it 'does not perform Geo::RepositorySyncService when secondary role is disabled' do
allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false } allow(Gitlab::Geo).to receive(:secondary_role_enabled?) { false }
expect(Geo::RepositorySyncService).not_to receive(:new) expect(Geo::RepositorySyncService).not_to receive(:new)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment