Commit b01bc0a0 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 25680-CI_ENVIRONMENT_URL

* upstream/master: (39 commits)
  Resolve "Improve Container Registry description"
  Add username parameter to gravatar URL
  Fix replying to a commit discussion displayed in the context of an MR
  Add fog-aliyun as backup storage provider
  Add missing specs
  Make sure protected can't be null; Test protected!
  Update session cookie key name to be unique to instance in development
  Just mention which GitLab version is required
  Fix data inconsistency issue for old artifacts by moving them to a currently used path
  Fix N+1 queries for non-members in comment threads
  Fix rubocop in spec/helpers/diff_helper_spec.rb
  Merge two items into one in the doc
  Only remove FK if it exists
  Maintain notes avatar at smaller breakpoint
  Fix pipeline schedule value name in documentation
  Add test for Project#protected_for?
  Update diff discussion position per discussion instead of per note
  Display Shared Runner status in Admin Dashboard
  Make sure we're loading the fresh variables
  Now secret_variables_for would return the variables
  ...
parents f6260328 1cd5c6d9
...@@ -97,6 +97,7 @@ gem 'fog-google', '~> 0.5' ...@@ -97,6 +97,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.8.6' gem 'google-api-client', '~> 0.8.6'
......
...@@ -213,6 +213,11 @@ GEM ...@@ -213,6 +213,11 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aliyun (0.1.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-aws (0.13.0) fog-aws (0.13.0)
fog-core (~> 1.38) fog-core (~> 1.38)
fog-json (~> 1.0) fog-json (~> 1.0)
...@@ -913,6 +918,7 @@ DEPENDENCIES ...@@ -913,6 +918,7 @@ DEPENDENCIES
flay (~> 2.8.0) flay (~> 2.8.0)
flipper (~> 0.10.2) flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2) flipper-active_record (~> 0.10.2)
fog-aliyun (~> 0.1.0)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
......
...@@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({ ...@@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({
}; };
}, },
computed: { computed: {
buttonText: function () {
if (this.discussionId) {
return 'Jump to next unresolved discussion';
} else {
return 'Jump to first unresolved discussion';
}
},
allResolved: function () { allResolved: function () {
return this.unresolvedDiscussionCount === 0; return this.unresolvedDiscussionCount === 0;
}, },
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
padding: 0; padding: 0;
&::before { &::before {
@include notes-media('max', $screen-xs-max) { @include notes-media('max', $screen-xs-min) {
background: none; background: none;
} }
} }
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
@include notes-media('max', $screen-xs-max) { @include notes-media('max', $screen-xs-min) {
.timeline-icon { .timeline-icon {
display: none; display: none;
} }
......
...@@ -42,6 +42,7 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -42,6 +42,7 @@ class Projects::VariablesController < Projects::ApplicationController
private private
def project_params def project_params
params.require(:variable).permit([:id, :key, :value, :_destroy]) params.require(:variable)
.permit([:id, :key, :value, :protected, :_destroy])
end end
end end
...@@ -66,12 +66,12 @@ module DiffHelper ...@@ -66,12 +66,12 @@ module DiffHelper
discussions_left = discussions_right = nil discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?) if left && (left.unchanged? || left.discussable?)
line_code = diff_file.line_code(left) line_code = diff_file.line_code(left)
discussions_left = @grouped_diff_discussions[line_code] discussions_left = @grouped_diff_discussions[line_code]
end end
if right && right.added? if right&.discussable?
line_code = diff_file.line_code(right) line_code = diff_file.line_code(right)
discussions_right = @grouped_diff_discussions[line_code] discussions_right = @grouped_diff_discussions[line_code]
end end
......
...@@ -50,7 +50,7 @@ module NotesHelper ...@@ -50,7 +50,7 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
data = { discussion_id: discussion.id, line_type: line_type } data = { discussion_id: discussion.reply_id, line_type: line_type }
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply' data: data, title: 'Add a reply'
......
...@@ -13,13 +13,13 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -13,13 +13,13 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters [\r\n] # any number of newline characters
}x }x
serialize :restricted_visibility_levels serialize :restricted_visibility_levels # rubocop:disable Cop/ActiverecordSerialize
serialize :import_sources serialize :import_sources # rubocop:disable Cop/ActiverecordSerialize
serialize :disabled_oauth_sign_in_sources, Array serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiverecordSerialize
serialize :domain_whitelist, Array serialize :domain_whitelist, Array # rubocop:disable Cop/ActiverecordSerialize
serialize :domain_blacklist, Array serialize :domain_blacklist, Array # rubocop:disable Cop/ActiverecordSerialize
serialize :repository_storages serialize :repository_storages # rubocop:disable Cop/ActiverecordSerialize
serialize :sidekiq_throttling_queues, Array serialize :sidekiq_throttling_queues, Array # rubocop:disable Cop/ActiverecordSerialize
cache_markdown_field :sign_in_text cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text cache_markdown_field :help_page_text
......
class AuditEvent < ActiveRecord::Base class AuditEvent < ActiveRecord::Base
serialize :details, Hash serialize :details, Hash # rubocop:disable Cop/ActiverecordSerialize
belongs_to :user, foreign_key: :author_id belongs_to :user, foreign_key: :author_id
......
...@@ -25,8 +25,8 @@ module Ci ...@@ -25,8 +25,8 @@ module Ci
project.environments.create(name: expanded_environment_name) project.environments.create(name: expanded_environment_name)
end end
serialize :options serialize :options # rubocop:disable Cop/ActiverecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiverecordSerialize
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
...@@ -208,7 +208,7 @@ module Ci ...@@ -208,7 +208,7 @@ module Ci
variables += project.deployment_variables if has_environment? variables += project.deployment_variables if has_environment?
variables += yaml_variables variables += yaml_variables
variables += user_variables variables += user_variables
variables += project.secret_variables variables += project.secret_variables_for(ref).map(&:to_runner_variable)
variables += trigger_request.user_variables if trigger_request variables += trigger_request.user_variables if trigger_request
variables variables
end end
...@@ -270,38 +270,6 @@ module Ci ...@@ -270,38 +270,6 @@ module Ci
Time.now - updated_at > 15.minutes.to_i Time.now - updated_at > 15.minutes.to_i
end end
##
# Deprecated
#
# This contains a hotfix for CI build data integrity, see #4246
#
# This method is used by `ArtifactUploader` to create a store_dir.
# Warning: Uploader uses it after AND before file has been stored.
#
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
# We need the project even if it's soft deleted, because whenever
# we're really deleting the project, we'll also delete the builds,
# and in order to delete the builds, we need to know where to find
# the artifacts, which is depending on the data of the project.
# We need to retain the project in this case.
the_project = project || unscoped_project
old = File.join(created_at.utc.strftime('%Y_%m'),
the_project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
return old if the_project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
the_project.id.to_s,
id.to_s
)
end
def valid_token?(token) def valid_token?(token)
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
......
...@@ -6,7 +6,7 @@ module Ci ...@@ -6,7 +6,7 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id belongs_to :pipeline, foreign_key: :commit_id
has_many :builds has_many :builds
serialize :variables serialize :variables # rubocop:disable Cop/ActiverecordSerialize
def user_variables def user_variables
return [] unless variables return [] unless variables
......
...@@ -12,11 +12,16 @@ module Ci ...@@ -12,11 +12,16 @@ module Ci
message: "can contain only letters, digits and '_'." } message: "can contain only letters, digits and '_'." }
scope :order_key_asc, -> { reorder(key: :asc) } scope :order_key_asc, -> { reorder(key: :asc) }
scope :unprotected, -> { where(protected: false) }
attr_encrypted :value, attr_encrypted :value,
mode: :per_attribute_iv_and_salt, mode: :per_attribute_iv_and_salt,
insecure_mode: true, insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base, key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
def to_runner_variable
{ key: key, value: value, public: false }
end
end end
end end
...@@ -10,6 +10,7 @@ class DiffDiscussion < Discussion ...@@ -10,6 +10,7 @@ class DiffDiscussion < Discussion
delegate :position, delegate :position,
:original_position, :original_position,
:change_position,
to: :first_note to: :first_note
......
...@@ -6,9 +6,9 @@ class DiffNote < Note ...@@ -6,9 +6,9 @@ class DiffNote < Note
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
serialize :original_position, Gitlab::Diff::Position serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
serialize :position, Gitlab::Diff::Position serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
serialize :change_position, Gitlab::Diff::Position serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
validates :original_position, presence: true validates :original_position, presence: true
validates :position, presence: true validates :position, presence: true
...@@ -95,13 +95,21 @@ class DiffNote < Note ...@@ -95,13 +95,21 @@ class DiffNote < Note
return if active? return if active?
Notes::DiffPositionUpdateService.new( tracer = Gitlab::Diff::PositionTracer.new(
self.project, project: self.project,
nil,
old_diff_refs: self.position.diff_refs, old_diff_refs: self.position.diff_refs,
new_diff_refs: noteable.diff_refs, new_diff_refs: self.noteable.diff_refs,
paths: self.position.paths paths: self.position.paths
).execute(self) )
result = tracer.trace(self.position)
return unless result
if result[:outdated]
self.change_position = result[:position]
else
self.position = result[:position]
end
end end
def verify_supported def verify_supported
......
...@@ -85,6 +85,12 @@ class Discussion ...@@ -85,6 +85,12 @@ class Discussion
first_note.discussion_id(context_noteable) first_note.discussion_id(context_noteable)
end end
def reply_id
# To reply to this discussion, we need the actual discussion_id from the database,
# not the potentially overwritten one based on the noteable.
first_note.discussion_id
end
alias_method :to_param, :id alias_method :to_param, :id
def diff_discussion? def diff_discussion?
......
...@@ -26,7 +26,7 @@ class Event < ActiveRecord::Base ...@@ -26,7 +26,7 @@ class Event < ActiveRecord::Base
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true
# For Hash only # For Hash only
serialize :data serialize :data # rubocop:disable Cop/ActiverecordSerialize
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
......
class WebHookLog < ActiveRecord::Base class WebHookLog < ActiveRecord::Base
belongs_to :web_hook belongs_to :web_hook
serialize :request_headers, Hash serialize :request_headers, Hash # rubocop:disable Cop/ActiverecordSerialize
serialize :request_data, Hash serialize :request_data, Hash # rubocop:disable Cop/ActiverecordSerialize
serialize :response_headers, Hash serialize :response_headers, Hash # rubocop:disable Cop/ActiverecordSerialize
validates :web_hook, presence: true validates :web_hook, presence: true
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
class LegacyDiffNote < Note class LegacyDiffNote < Note
include NoteOnDiff include NoteOnDiff
serialize :st_diff serialize :st_diff # rubocop:disable Cop/ActiverecordSerialize
validates :line_code, presence: true, line_code: true validates :line_code, presence: true, line_code: true
......
...@@ -21,7 +21,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -21,7 +21,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
serialize :merge_params, Hash serialize :merge_params, Hash # rubocop:disable Cop/ActiverecordSerialize
after_create :ensure_merge_request_diff, unless: :importing? after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed after_update :reload_diff_if_branch_changed
...@@ -421,7 +421,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -421,7 +421,7 @@ class MergeRequest < ActiveRecord::Base
MergeRequests::MergeRequestDiffCacheService.new.execute(self) MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs new_diff_refs = self.diff_refs
update_diff_notes_positions( update_diff_discussion_positions(
old_diff_refs: old_diff_refs, old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs, new_diff_refs: new_diff_refs,
current_user: current_user current_user: current_user
...@@ -853,19 +853,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -853,19 +853,18 @@ class MergeRequest < ActiveRecord::Base
diff_refs && diff_refs.complete? diff_refs && diff_refs.complete?
end end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil) def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
return unless has_complete_diff_refs? return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs return if new_diff_refs == old_diff_refs
active_diff_notes = self.notes.new_diff_notes.select do |note| active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
note.active?(old_diff_refs) discussion.active?(old_diff_refs)
end end
return if active_diff_discussions.empty?
return if active_diff_notes.empty? paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq service = Discussions::UpdateDiffPositionService.new(
service = Notes::DiffPositionUpdateService.new(
self.project, self.project,
current_user, current_user,
old_diff_refs: old_diff_refs, old_diff_refs: old_diff_refs,
...@@ -873,11 +872,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -873,11 +872,8 @@ class MergeRequest < ActiveRecord::Base
paths: paths paths: paths
) )
transaction do active_diff_discussions.each do |discussion|
active_diff_notes.each do |note| service.execute(discussion)
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
end
end end
end end
......
...@@ -11,8 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -11,8 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request belongs_to :merge_request
serialize :st_commits serialize :st_commits # rubocop:disable Cop/ActiverecordSerialize
serialize :st_diffs serialize :st_diffs # rubocop:disable Cop/ActiverecordSerialize
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
state :collected state :collected
......
...@@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :token add_authentication_token_field :token
serialize :scopes, Array serialize :scopes, Array # rubocop:disable Cop/ActiverecordSerialize
belongs_to :user belongs_to :user
......
...@@ -1245,12 +1245,19 @@ class Project < ActiveRecord::Base ...@@ -1245,12 +1245,19 @@ class Project < ActiveRecord::Base
variables variables
end end
def secret_variables def secret_variables_for(ref)
variables.map do |variable| if protected_for?(ref)
{ key: variable.key, value: variable.value, public: false } variables
else
variables.unprotected
end end
end end
def protected_for?(ref)
ProtectedBranch.protected?(self, ref) ||
ProtectedTag.protected?(self, ref)
end
def deployment_variables def deployment_variables
return [] unless deployment_service return [] unless deployment_service
......
...@@ -10,7 +10,7 @@ class ProjectImportData < ActiveRecord::Base ...@@ -10,7 +10,7 @@ class ProjectImportData < ActiveRecord::Base
insecure_mode: true, insecure_mode: true,
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
serialize :data, JSON serialize :data, JSON # rubocop:disable Cop/ActiverecordSerialize
validates :project, presence: true validates :project, presence: true
......
...@@ -167,7 +167,7 @@ class ProjectTeam ...@@ -167,7 +167,7 @@ class ProjectTeam
access = RequestStore.store[key] access = RequestStore.store[key]
end end
# Lookup only the IDs we need # Look up only the IDs we need
user_ids = user_ids - access.keys user_ids = user_ids - access.keys
return access if user_ids.empty? return access if user_ids.empty?
...@@ -178,6 +178,13 @@ class ProjectTeam ...@@ -178,6 +178,13 @@ class ProjectTeam
maximum(:access_level) maximum(:access_level)
access.merge!(users_access) access.merge!(users_access)
missing_user_ids = user_ids - users_access.keys
missing_user_ids.each do |user_id|
access[user_id] = Gitlab::Access::NO_ACCESS
end
access access
end end
......
class SentNotification < ActiveRecord::Base class SentNotification < ActiveRecord::Base
serialize :position, Gitlab::Diff::Position serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
belongs_to :project belongs_to :project
belongs_to :noteable, polymorphic: true belongs_to :noteable, polymorphic: true
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# and implement a set of methods # and implement a set of methods
class Service < ActiveRecord::Base class Service < ActiveRecord::Base
include Sortable include Sortable
serialize :properties, JSON serialize :properties, JSON # rubocop:disable Cop/ActiverecordSerialize
default_value_for :active, false default_value_for :active, false
default_value_for :push_events, true default_value_for :push_events, true
......
...@@ -40,7 +40,7 @@ class User < ActiveRecord::Base ...@@ -40,7 +40,7 @@ class User < ActiveRecord::Base
otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10 devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiverecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable, devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable :validatable, :omniauthable, :confirmable, :registerable
...@@ -781,7 +781,7 @@ class User < ActiveRecord::Base ...@@ -781,7 +781,7 @@ class User < ActiveRecord::Base
def avatar_url(size: nil, scale: 2, **args) def avatar_url(size: nil, scale: 2, **args)
# We use avatar_path instead of overriding avatar_url because of carrierwave. # We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864 # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
avatar_path(args) || GravatarService.new.execute(email, size, scale) avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end end
def all_emails def all_emails
......
module Discussions
class UpdateDiffPositionService < BaseService
def execute(discussion)
result = tracer.trace(discussion.position)
return unless result
position = result[:position]
outdated = result[:outdated]
discussion.notes.each do |note|
if outdated
note.change_position = position
else
note.position = position
note.change_position = nil
end
end
Note.transaction do
discussion.notes.each do |note|
Gitlab::Timeless.timeless(note, &:save)
end
if outdated && current_user
SystemNoteService.diff_discussion_outdated(discussion, project, current_user, position)
end
end
end
private
def tracer
@tracer ||= Gitlab::Diff::PositionTracer.new(
project: project,
old_diff_refs: params[:old_diff_refs],
new_diff_refs: params[:new_diff_refs],
paths: params[:paths]
)
end
end
end
class GravatarService class GravatarService
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
def execute(email, size = nil, scale = 2) def execute(email, size = nil, scale = 2, username: nil)
if current_application_settings.gravatar_enabled? && email.present? return unless current_application_settings.gravatar_enabled?
size = 40 if size.nil? || size <= 0
identifier = email.presence || username.presence
return unless identifier
hash = Digest::MD5.hexdigest(identifier.strip.downcase)
size = 40 unless size && size > 0
sprintf gravatar_url, sprintf gravatar_url,
hash: Digest::MD5.hexdigest(email.strip.downcase), hash: hash,
size: size * scale, size: size * scale,
email: email.strip email: ERB::Util.url_encode(email&.strip || ''),
end username: ERB::Util.url_encode(username&.strip || '')
end end
def gitlab_config def gitlab_config
......
module Notes
class DiffPositionUpdateService < BaseService
def execute(note)
results = tracer.trace(note.position)
return unless results
position = results[:position]
outdated = results[:outdated]
if outdated
note.change_position = position
if note.persisted? && current_user
SystemNoteService.diff_discussion_outdated(note.to_discussion, project, current_user, position)
end
else
note.position = position
note.change_position = nil
end
end
private
def tracer
@tracer ||= Gitlab::Diff::PositionTracer.new(
project: project,
old_diff_refs: params[:old_diff_refs],
new_diff_refs: params[:new_diff_refs],
paths: params[:paths]
)
end
end
end
class ArtifactUploader < GitlabUploader class ArtifactUploader < GitlabUploader
storage :file storage :file
attr_accessor :build, :field attr_reader :job, :field
def self.artifacts_path def self.local_artifacts_store
Gitlab.config.artifacts.path Gitlab.config.artifacts.path
end end
def self.artifacts_upload_path def self.artifacts_upload_path
File.join(self.artifacts_path, 'tmp/uploads/') File.join(self.local_artifacts_store, 'tmp/uploads/')
end end
def self.artifacts_cache_path def initialize(job, field)
File.join(self.artifacts_path, 'tmp/cache/') @job, @field = job, field
end
def initialize(build, field)
@build, @field = build, field
end end
def store_dir def store_dir
File.join(self.class.artifacts_path, @build.artifacts_path) default_local_path
end end
def cache_dir def cache_dir
File.join(self.class.artifacts_cache_path, @build.artifacts_path) File.join(self.class.local_artifacts_store, 'tmp/cache')
end
private
def default_local_path
File.join(self.class.local_artifacts_store, default_path)
end end
def filename def default_path
file.try(:filename) File.join(job.created_at.utc.strftime('%Y_%m'), job.project_id.to_s, job.id.to_s)
end end
end end
...@@ -10,7 +10,11 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -10,7 +10,11 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, to: :class delegate :base_dir, to: :class
def file_storage? def file_storage?
self.class.storage == CarrierWave::Storage::File storage.is_a?(CarrierWave::Storage::File)
end
def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File)
end end
# Reduce disk IO # Reduce disk IO
......
...@@ -79,6 +79,12 @@ ...@@ -79,6 +79,12 @@
= gitlab_pages = gitlab_pages
%span.light.pull-right %span.light.pull-right
= boolean_to_icon gitlab_pages_enabled = boolean_to_icon gitlab_pages_enabled
- gitlab_shared_runners = 'Shared Runners'
- gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled
%p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") }
= gitlab_shared_runners
%span.light.pull-right
= boolean_to_icon gitlab_shared_runners_enabled
.col-md-4 .col-md-4
%h4 %h4
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" } %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
.btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" } .btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion", ":title" => "buttonText",
"aria-label" => "Jump to next unresolved discussion", ":aria-label" => "buttonText",
data: { container: "body" } } data: { container: "body" } }
= custom_icon("next_discussion") = custom_icon("next_discussion")
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- discussions = local_assigns.fetch(:discussions, nil) - discussions = local_assigns.fetch(:discussions, nil)
- type = line.type - type = line.type
- line_code = diff_file.line_code(line) - line_code = diff_file.line_code(line)
- if discussions && !line.meta? - if discussions && line.discussable?
- line_discussions = discussions[line_code] - line_discussions = discussions[line_code]
%tr.line_holder{ class: type, id: (line_code unless plain) } %tr.line_holder{ class: type, id: (line_code unless plain) }
- case type - case type
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block %p.help-block
Per job in minutes. If a job passes this threshold, it will be marked as failed. Per job in minutes. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr %hr
......
- page_title "Container Registry" - page_title "Container Registry"
%hr .row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
With the Docker Container Registry integrated into GitLab, every project
can have its own space to store its Docker images.
%p.append-bottom-0
= succeed '.' do
Learn more about
= link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
%ul.content-list .col-lg-9
%li.light.prepend-top-default .panel.panel-default
.panel-heading
%h4.panel-title
How to use the Container Registry
.panel-body
%p %p
A 'container image' is a snapshot of a container. First log in to GitLab&rsquo;s Container Registry using your GitLab username
You can host your container images with GitLab. and password. If you have
%br = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
To start using container images hosted on GitLab you first need to login: you need to use a
= succeed ':' do
= link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
%pre %pre
%code
docker login #{Gitlab.config.registry.host_port} docker login #{Gitlab.config.registry.host_port}
%br %br
Then you are free to create and upload a container image with build and push commands: %p
Once you log in, you&rsquo;re free to create and upload a container image
using the common
%code build
and
%code push
commands:
%pre %pre
docker build -t #{escape_once(@project.container_registry_url)}/image . :plain
%br docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}/image docker push #{escape_once(@project.container_registry_url)}
- if @images.blank? %hr
.nothing-here-block No container image repositories in Container Registry for this project. %h5.prepend-top-default
Use different image names
%p.light
GitLab supports up to 3 levels of image names. The following
examples of images are valid for your project:
%pre
:plain
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
- if @images.blank?
%p.settings-message.text-center.append-bottom-default
No container images stored for this project. Add one by following the
instructions above.
- else - else
= render partial: 'image', collection: @images = render partial: 'image', collection: @images
%h4.prepend-top-0 %h4.prepend-top-0
Secret Variables Secret variables
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank'
%p %p
These variables will be set to environment by the runner. These variables will be set to environment by the runner, and could be protected by exposing only to protected branches or tags.
%p %p
So you can use them for passwords, secret keys or whatever you want. So you can use them for passwords, secret keys or whatever you want.
%p %p
......
...@@ -7,4 +7,13 @@ ...@@ -7,4 +7,13 @@
.form-group .form-group
= f.label :value, "Value", class: "label-light" = f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE" = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
.form-group
.checkbox
= f.label :protected do
= f.check_box :protected
%strong Protected
.help-block
This variable will be passed only to pipelines running on protected branches and tags
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'protected-secret-variables'), target: '_blank'
= f.submit btn_text, class: "btn btn-save" = f.submit btn_text, class: "btn btn-save"
.table-responsive.variables-table .table-responsive.variables-table
%table.table %table.table
%colgroup %colgroup
%col
%col %col
%col %col
%col{ width: 100 } %col{ width: 100 }
%thead %thead
%th Key %th Key
%th Value %th Value
%th Protected
%th %th
%tbody %tbody
- @project.variables.order_key_asc.each do |variable| - @project.variables.order_key_asc.each do |variable|
...@@ -14,6 +16,7 @@ ...@@ -14,6 +16,7 @@
%tr %tr
%td.variable-key= variable.key %td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }****** %td.variable-value{ "data-value" => variable.value }******
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu %td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only %span.sr-only
......
---
title: Add protected variables which would only be passed to protected branches or
protected tags
merge_request: 11688
author:
---
title: Add changelog for improved Registry description
merge_request: 11816
author:
---
title: Display Shared Runner status in Admin Dashboard
merge_request: 11783
author: Ivan Chernov
---
title: Update session cookie key name to be unique to instance in development
merge_request:
author:
---
title: Add Aliyun OSS as the backup storage provider
merge_request: 9721
author: Yuanfei Zhu
---
title: "Fixed handling of the `can_push` attribute in the v3 deploy_keys api"
merge_request: 11607
author: Richard Clamp
---
title: Fix replying to a commit discussion displayed in the context of an MR
merge_request:
author:
---
title: Fix title of discussion jump button at top of page
merge_request:
author:
---
title: Add username parameter to gravatar URL
merge_request:
author:
---
title: Fix N+1 queries for non-members in comment threads
merge_request:
author:
---
title: 'Fix: A diff comment on a change at last line of a file shows as two comments
in discussion'
merge_request:
author:
---
title: Migrate artifacts to a new path
merge_request:
author:
---
title: Remove foreigh key on ci_trigger_schedules only if it exists
merge_request:
author:
...@@ -169,7 +169,7 @@ production: &base ...@@ -169,7 +169,7 @@ production: &base
## Gravatar ## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar: gravatar:
# gravatar urls: possible placeholders: %{hash} %{size} %{email} # gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username}
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
......
...@@ -10,6 +10,12 @@ rescue ...@@ -10,6 +10,12 @@ rescue
Settings.gitlab['session_expire_delay'] ||= 10080 Settings.gitlab['session_expire_delay'] ||= 10080
end end
cookie_key = if Rails.env.development?
"_gitlab_session_#{Digest::SHA256.hexdigest(Rails.root.to_s)}"
else
"_gitlab_session"
end
if Rails.env.test? if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else else
...@@ -19,7 +25,7 @@ else ...@@ -19,7 +25,7 @@ else
Gitlab::Application.config.session_store( Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks. :redis_store, # Using the cookie_store would enable session replay attacks.
servers: redis_config, servers: redis_config,
key: '_gitlab_session', key: cookie_key,
secure: Gitlab.config.gitlab.https, secure: Gitlab.config.gitlab.https,
httponly: true, httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60, expires_in: Settings.gitlab['session_expire_delay'] * 60,
......
...@@ -4,10 +4,20 @@ class RemoveForeighKeyCiTriggerSchedules < ActiveRecord::Migration ...@@ -4,10 +4,20 @@ class RemoveForeighKeyCiTriggerSchedules < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
if fk_on_trigger_schedules?
remove_foreign_key :ci_trigger_schedules, column: :trigger_id remove_foreign_key :ci_trigger_schedules, column: :trigger_id
end end
end
def down def down
# no op, the foreign key should not have been here # no op, the foreign key should not have been here
end end
private
# Not made more generic and lifted to the helpers as Rails 5 will provide
# such an API
def fk_on_trigger_schedules?
connection.foreign_keys(:ci_trigger_schedules).include?("ci_triggers")
end
end end
class AddProtectedToCiVariables < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:ci_variables, :protected, :boolean, default: false)
end
def down
remove_column(:ci_variables, :protected)
end
end
class MigrateOldArtifacts < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
# This uses special heuristic to find potential candidates for data migration
# Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345
def up
builds_with_artifacts.find_each do |build|
build.migrate_artifacts!
end
end
def down
end
private
def builds_with_artifacts
Build.with_artifacts
.joins('JOIN projects ON projects.id = ci_builds.project_id')
.where('ci_builds.id < ?', min_id)
.where('projects.ci_id IS NOT NULL')
.select('id', 'created_at', 'project_id', 'projects.ci_id AS ci_id')
end
def min_id
Build.joins('JOIN projects ON projects.id = ci_builds.project_id')
.where('projects.ci_id IS NULL')
.pluck('coalesce(min(ci_builds.id), 0)')
.first
end
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
scope :with_artifacts, -> { where.not(artifacts_file: [nil, '']) }
def migrate_artifacts!
return unless File.exist?(source_artifacts_path)
return if File.exist?(target_artifacts_path)
ensure_target_path
FileUtils.move(source_artifacts_path, target_artifacts_path)
end
private
def source_artifacts_path
@source_artifacts_path ||=
File.join(Gitlab.config.artifacts.path,
created_at.utc.strftime('%Y_%m'),
ci_id.to_s, id.to_s)
end
def target_artifacts_path
@target_artifacts_path ||=
File.join(Gitlab.config.artifacts.path,
created_at.utc.strftime('%Y_%m'),
project_id.to_s, id.to_s)
end
def ensure_target_path
directory = File.dirname(target_artifacts_path)
FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
end
end
end
...@@ -356,6 +356,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do ...@@ -356,6 +356,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do
t.string "encrypted_value_salt" t.string "encrypted_value_salt"
t.string "encrypted_value_iv" t.string "encrypted_value_iv"
t.integer "project_id", null: false t.integer "project_id", null: false
t.boolean "protected", default: false, null: false
end end
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
......
...@@ -62,10 +62,11 @@ POST /projects/:id/variables ...@@ -62,10 +62,11 @@ POST /projects/:id/variables
``` ```
| Attribute | Type | required | Description | | Attribute | Type | required | Description |
|-----------|---------|----------|-----------------------| |-------------|---------|----------|-----------------------|
| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed | | `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable | | `value` | string | yes | The `value` of a variable |
| `protected` | boolean | no | Whether the variable is protected |
``` ```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
...@@ -74,7 +75,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl ...@@ -74,7 +75,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl
```json ```json
{ {
"key": "NEW_VARIABLE", "key": "NEW_VARIABLE",
"value": "new value" "value": "new value",
"protected": false
} }
``` ```
...@@ -87,10 +89,11 @@ PUT /projects/:id/variables/:key ...@@ -87,10 +89,11 @@ PUT /projects/:id/variables/:key
``` ```
| Attribute | Type | required | Description | | Attribute | Type | required | Description |
|-----------|---------|----------|-------------------------| |-------------|---------|----------|-------------------------|
| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable | | `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable | | `value` | string | yes | The `value` of a variable |
| `protected` | boolean | no | Whether the variable is protected |
``` ```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value" curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
...@@ -99,7 +102,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitla ...@@ -99,7 +102,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitla
```json ```json
{ {
"key": "NEW_VARIABLE", "key": "NEW_VARIABLE",
"value": "updated value" "value": "updated value",
"protected": true
} }
``` ```
......
...@@ -10,7 +10,7 @@ The variables can be overwritten and they take precedence over each other in ...@@ -10,7 +10,7 @@ The variables can be overwritten and they take precedence over each other in
this order: this order:
1. [Trigger variables][triggers] (take precedence over all) 1. [Trigger variables][triggers] (take precedence over all)
1. [Secret variables](#secret-variables) 1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables) 1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables) 1. [Deployment variables](#deployment-variables)
...@@ -154,9 +154,25 @@ storing things like passwords, secret keys and credentials. ...@@ -154,9 +154,25 @@ storing things like passwords, secret keys and credentials.
Secret variables can be added by going to your project's Secret variables can be added by going to your project's
**Settings ➔ Pipelines**, then finding the section called **Settings ➔ Pipelines**, then finding the section called
**Secret Variables**. **Secret variables**.
Once you set them, they will be available for all subsequent jobs. Once you set them, they will be available for all subsequent pipelines.
## Protected secret variables
>**Notes:**
This feature requires GitLab 9.3 or higher.
Secret variables could be protected. Whenever a secret variable is
protected, it would only be securely passed to pipelines running on the
[protected branches] or [protected tags]. The other pipelines would not get any
protected variables.
Protected variables can be added by going to your project's
**Settings ➔ Pipelines**, then finding the section called
**Secret variables**, and check *Protected*.
Once you set them, they will be available for all subsequent pipelines.
## Deployment variables ## Deployment variables
...@@ -386,3 +402,5 @@ export CI_REGISTRY_PASSWORD="longalfanumstring" ...@@ -386,3 +402,5 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[runner]: https://docs.gitlab.com/runner/ [runner]: https://docs.gitlab.com/runner/
[triggered]: ../triggers/README.md [triggered]: ../triggers/README.md
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger [triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
...@@ -16,7 +16,7 @@ the configuration options as follows: ...@@ -16,7 +16,7 @@ the configuration options as follows:
```yml ```yml
gravatar: gravatar:
enabled: true enabled: true
# gravatar URLs: possible placeholders: %{hash} %{size} %{email} # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
``` ```
...@@ -25,7 +25,7 @@ the configuration options as follows: ...@@ -25,7 +25,7 @@ the configuration options as follows:
```yml ```yml
gravatar: gravatar:
enabled: true enabled: true
# gravatar URLs: possible placeholders: %{hash} %{size} %{email} # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
``` ```
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
- [Adding database indexes](adding_database_indexes.md) - [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md) - [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign Keys & Associations](foreign_keys.md) - [Foreign Keys & Associations](foreign_keys.md)
- [Serializing Data](serializing_data.md)
## i18n ## i18n
......
# Serializing Data
**Summary:** don't store serialized data in the database, use separate columns
and/or tables instead.
Rails makes it possible to store serialized data in JSON, YAML or other formats.
Such a field can be defined as follows:
```ruby
class Issue < ActiveRecord::Model
serialize :custom_fields
end
```
While it may be tempting to store serialized data in the database there are many
problems with this. This document will outline these problems and provide an
alternative.
## Serialized Data Is Less Powerful
When using a relational database you have the ability to query individual
fields, change the schema, index data and so forth. When you use serialized data
all of that becomes either very difficult or downright impossible. While
PostgreSQL does offer the ability to query JSON fields it is mostly meant for
very specialized use cases, and not for more general use. If you use YAML in
turn there's no way to query the data at all.
## Waste Of Space
Storing serialized data such as JSON or YAML will end up wasting a lot of space.
This is because these formats often include additional characters (e.g. double
quotes or newlines) besides the data that you are storing.
## Difficult To Manage
There comes a time where you will need to add a new field to the serialized
data, or change an existing one. Using serialized data this becomes difficult
and very time consuming as the only way of doing so is to re-write all the
stored values. To do so you would have to:
1. Retrieve the data
1. Parse it into a Ruby structure
1. Mutate it
1. Serialize it back to a String
1. Store it in the database
On the other hand, if one were to use regular columns adding a column would be
as easy as this:
```sql
ALTER TABLE table_name ADD COLUMN column_name type;
```
Such a query would take very little to no time and would immediately apply to
all rows, without having to re-write large JSON or YAML structures.
Finally, there comes a time when the JSON or YAML structure is no longer
sufficient and you need to migrate away from it. When storing only a few rows
this may not be a problem, but when storing millions of rows such a migration
can easily take hours or even days to complete.
## Relational Databases Are Not Document Stores
When storing data as JSON or YAML you're essentially using your database as if
it were a document store (e.g. MongoDB), except you're not using any of the
powerful features provided by a typical RDBMS _nor_ are you using any of the
features provided by a typical document store (e.g. the ability to index fields
of documents with variable fields). In other words, it's a waste.
## Consistent Fields
One argument sometimes made in favour of serialized data is having to store
widely varying fields and values. Sometimes this is truly the case, and then
perhaps it might make sense to use serialized data. However, in 99% of the cases
the fields and types stored tend to be the same for every row. Even if there is
a slight difference you can still use separate columns and just not set the ones
you don't need.
## The Solution
The solution is very simple: just use separate columns and/or separate tables.
This will allow you to use all the features provided by your database, it will
make it easier to manage and migrate the data, you'll conserve space, you can
index the data efficiently and so forth.
...@@ -133,7 +133,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload. ...@@ -133,7 +133,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab [other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88) [imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares). [also available](#uploading-to-locally-mounted-shares).
For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`: For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
......
...@@ -95,8 +95,6 @@ and click **Registry** in the project menu. ...@@ -95,8 +95,6 @@ and click **Registry** in the project menu.
This view will show you all tags in your project and will easily allow you to This view will show you all tags in your project and will easily allow you to
delete them. delete them.
![Container Registry panel](img/container_registry_panel.png)
## Build and push images using GitLab CI ## Build and push images using GitLab CI
> **Note:** > **Note:**
......
...@@ -53,7 +53,7 @@ Sidekiq, which runs according to its interval. For example, if you set a ...@@ -53,7 +53,7 @@ Sidekiq, which runs according to its interval. For example, if you set a
schedule to create a pipeline every minute (`* * * * *`) and the Sidekiq worker schedule to create a pipeline every minute (`* * * * *`) and the Sidekiq worker
runs on 00:00 and 12:00 every day (`0 */12 * * *`), only 2 pipelines will be runs on 00:00 and 12:00 every day (`0 */12 * * *`), only 2 pipelines will be
created per day. To change the Sidekiq worker's frequency, you have to edit the created per day. To change the Sidekiq worker's frequency, you have to edit the
`trigger_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab. `pipeline_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab.
For GitLab.com, you can check the [dedicated settings page][settings]. If you For GitLab.com, you can check the [dedicated settings page][settings]. If you
don't have admin access to the server, ask your administrator. don't have admin access to the server, ask your administrator.
......
...@@ -675,6 +675,7 @@ module API ...@@ -675,6 +675,7 @@ module API
class Variable < Grape::Entity class Variable < Grape::Entity
expose :key, :value expose :key, :value
expose :protected?, as: :protected
end end
class Pipeline < PipelineBasic class Pipeline < PipelineBasic
......
...@@ -311,6 +311,16 @@ module API ...@@ -311,6 +311,16 @@ module API
end end
end end
def present_artifacts!(artifacts_file)
return not_found! unless artifacts_file.exists?
if artifacts_file.file_storage?
present_file!(artifacts_file.path, artifacts_file.filename)
else
redirect_to(artifacts_file.url)
end
end
private private
def private_token def private_token
......
...@@ -224,16 +224,6 @@ module API ...@@ -224,16 +224,6 @@ module API
find_build(id) || not_found! find_build(id) || not_found!
end end
def present_artifacts!(artifacts_file)
if !artifacts_file.file_storage?
redirect_to(build.artifacts_file.url)
elsif artifacts_file.exists?
present_file!(artifacts_file.path, artifacts_file.filename)
else
not_found!
end
end
def filter_builds(builds, scope) def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty? return builds if scope.nil? || scope.empty?
......
...@@ -241,16 +241,7 @@ module API ...@@ -241,16 +241,7 @@ module API
get '/:id/artifacts' do get '/:id/artifacts' do
job = authenticate_job! job = authenticate_job!
artifacts_file = job.artifacts_file present_artifacts!(job.artifacts_file)
unless artifacts_file.file_storage?
return redirect_to job.artifacts_file.url
end
unless artifacts_file.exists?
not_found!
end
present_file!(artifacts_file.path, artifacts_file.filename)
end end
end end
end end
......
...@@ -225,16 +225,6 @@ module API ...@@ -225,16 +225,6 @@ module API
find_build(id) || not_found! find_build(id) || not_found!
end end
def present_artifacts!(artifacts_file)
if !artifacts_file.file_storage?
redirect_to(build.artifacts_file.url)
elsif artifacts_file.exists?
present_file!(artifacts_file.path, artifacts_file.filename)
else
not_found!
end
end
def filter_builds(builds, scope) def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty? return builds if scope.nil? || scope.empty?
......
...@@ -41,6 +41,7 @@ module API ...@@ -41,6 +41,7 @@ module API
params do params do
requires :key, type: String, desc: 'The new deploy key' requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key' requires :title, type: String, desc: 'The name of the deploy key'
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end end
post ":id/#{path}" do post ":id/#{path}" do
params[:key].strip! params[:key].strip!
......
...@@ -42,6 +42,7 @@ module API ...@@ -42,6 +42,7 @@ module API
params do params do
requires :key, type: String, desc: 'The key of the variable' requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable' requires :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
end end
post ':id/variables' do post ':id/variables' do
variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h) variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
...@@ -59,13 +60,14 @@ module API ...@@ -59,13 +60,14 @@ module API
params do params do
optional :key, type: String, desc: 'The key of the variable' optional :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable' optional :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
end end
put ':id/variables/:key' do put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key]) variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable return not_found!('Variable') unless variable
if variable.update(value: params[:value]) if variable.update(declared_params(include_missing: false).except(:key))
present variable, with: Entities::Variable present variable, with: Entities::Variable
else else
render_validation_error!(variable) render_validation_error!(variable)
......
...@@ -3,7 +3,7 @@ require 'backup/files' ...@@ -3,7 +3,7 @@ require 'backup/files'
module Backup module Backup
class Artifacts < Files class Artifacts < Files
def initialize def initialize
super('artifacts', ArtifactUploader.artifacts_path) super('artifacts', ArtifactUploader.local_artifacts_store)
end end
def create_files_dir def create_files_dir
......
...@@ -187,14 +187,14 @@ module Ci ...@@ -187,14 +187,14 @@ module Ci
build = authenticate_build! build = authenticate_build!
artifacts_file = build.artifacts_file artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
return redirect_to build.artifacts_file.url
end
unless artifacts_file.exists? unless artifacts_file.exists?
not_found! not_found!
end end
unless artifacts_file.file_storage?
return redirect_to build.artifacts_file.url
end
present_file!(artifacts_file.path, artifacts_file.filename) present_file!(artifacts_file.path, artifacts_file.filename)
end end
......
...@@ -59,6 +59,10 @@ module Gitlab ...@@ -59,6 +59,10 @@ module Gitlab
type == 'match' type == 'match'
end end
def discussable?
!['match', 'new-nonewline', 'old-nonewline'].include?(type)
end
def as_json(opts = nil) def as_json(opts = nil)
{ {
type: type, type: type,
......
...@@ -21,5 +21,13 @@ module Gitlab ...@@ -21,5 +21,13 @@ module Gitlab
nil nil
end end
def boolean_to_yes_no(bool)
if bool
'Yes'
else
'No'
end
end
end end
end end
module RuboCop
module Cop
# Cop that prevents the use of `serialize` in ActiveRecord models.
class ActiverecordSerialize < RuboCop::Cop::Cop
MSG = 'Do not store serialized data in the database, use separate columns and/or tables instead'.freeze
def on_send(node)
return unless in_models?(node)
add_offense(node, :selector) if node.children[1] == :serialize
end
def models_path
File.join(Dir.pwd, 'app', 'models')
end
def in_models?(node)
path = node.location.expression.source_buffer.name
path.start_with?(models_path)
end
end
end
end
require_relative 'cop/custom_error_class' require_relative 'cop/custom_error_class'
require_relative 'cop/gem_fetcher' require_relative 'cop/gem_fetcher'
require_relative 'cop/activerecord_serialize'
require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_foreign_key'
......
...@@ -3,6 +3,10 @@ FactoryGirl.define do ...@@ -3,6 +3,10 @@ FactoryGirl.define do
sequence(:key) { |n| "VARIABLE_#{n}" } sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE' value 'VARIABLE_VALUE'
trait(:protected) do
protected true
end
project factory: :empty_project project factory: :empty_project
end end
end end
...@@ -19,7 +19,7 @@ describe "Container Registry" do ...@@ -19,7 +19,7 @@ describe "Container Registry" do
scenario 'user visits container registry main page' do scenario 'user visits container registry main page' do
visit_container_registry visit_container_registry
expect(page).to have_content 'No container image repositories' expect(page).to have_content 'No container images'
end end
end end
......
...@@ -5,7 +5,7 @@ feature 'Merge Request Discussions', feature: true do ...@@ -5,7 +5,7 @@ feature 'Merge Request Discussions', feature: true do
login_as :admin login_as :admin
end end
context "Diff discussions" do describe "Diff discussions" do
let(:merge_request) { create(:merge_request, importing: true) } let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project } let(:project) { merge_request.source_project }
let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) } let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) }
...@@ -48,4 +48,43 @@ feature 'Merge Request Discussions', feature: true do ...@@ -48,4 +48,43 @@ feature 'Merge Request Discussions', feature: true do
end end
end end
end end
describe 'Commit comments displayed in MR context', :js do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
shared_examples 'a functional discussion' do
let(:discussion_id) { note.discussion_id(merge_request) }
it 'is displayed' do
expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
end
it 'can be replied to' do
within(".discussion[data-discussion-id='#{discussion_id}']") do
click_button 'Reply...'
fill_in 'note[note]', with: 'Test!'
click_button 'Comment'
expect(page).to have_css('.note', count: 2)
end
end
end
before(:each) do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
context 'a regular commit comment' do
let(:note) { create(:note_on_commit, project: project) }
it_behaves_like 'a functional discussion'
end
context 'a commit diff comment' do
let(:note) { create(:diff_note_on_commit, project: project) }
it_behaves_like 'a functional discussion'
end
end
end end
...@@ -19,7 +19,7 @@ describe 'Project variables', js: true do ...@@ -19,7 +19,7 @@ describe 'Project variables', js: true do
end end
end end
it 'adds new variable' do it 'adds new secret variable' do
fill_in('variable_key', with: 'key') fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value') fill_in('variable_value', with: 'key value')
click_button('Add new variable') click_button('Add new variable')
...@@ -27,6 +27,7 @@ describe 'Project variables', js: true do ...@@ -27,6 +27,7 @@ describe 'Project variables', js: true do
expect(page).to have_content('Variables were successfully updated.') expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do page.within('.variables-table') do
expect(page).to have_content('key') expect(page).to have_content('key')
expect(page).to have_content('No')
end end
end end
...@@ -41,6 +42,19 @@ describe 'Project variables', js: true do ...@@ -41,6 +42,19 @@ describe 'Project variables', js: true do
end end
end end
it 'adds new protected variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'value')
check('Protected')
click_button('Add new variable')
expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('Yes')
end
end
it 'reveals and hides new variable' do it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key') fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value') fill_in('variable_value', with: 'key value')
...@@ -85,7 +99,7 @@ describe 'Project variables', js: true do ...@@ -85,7 +99,7 @@ describe 'Project variables', js: true do
click_button('Save variable') click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.') expect(page).to have_content('Variable was successfully updated.')
expect(project.variables.first.value).to eq('key value') expect(project.variables(true).first.value).to eq('key value')
end end
it 'edits variable with empty value' do it 'edits variable with empty value' do
...@@ -98,6 +112,34 @@ describe 'Project variables', js: true do ...@@ -98,6 +112,34 @@ describe 'Project variables', js: true do
click_button('Save variable') click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.') expect(page).to have_content('Variable was successfully updated.')
expect(project.variables.first.value).to eq('') expect(project.variables(true).first.value).to eq('')
end
it 'edits variable to be protected' do
page.within('.variables-table') do
find('.btn-variable-edit').click
end
expect(page).to have_content('Update variable')
check('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).to be_protected
end
it 'edits variable to be unprotected' do
project.variables.first.update(protected: true)
page.within('.variables-table') do
find('.btn-variable-edit').click
end
expect(page).to have_content('Update variable')
uncheck('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).not_to be_protected
end end
end end
...@@ -129,6 +129,33 @@ describe DiffHelper do ...@@ -129,6 +129,33 @@ describe DiffHelper do
end end
end end
describe '#parallel_diff_discussions' do
let(:discussion) { { 'abc_3_3' => 'comment' } }
let(:diff_file) { double(line_code: 'abc_3_3') }
before do
helper.instance_variable_set(:@grouped_diff_discussions, discussion)
end
it 'does not put comments on nonewline lines' do
left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
right = Gitlab::Diff::Line.new('\\nonewline', 'new-nonewline', 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq([nil, nil])
end
it 'puts comments on added lines' do
left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq([nil, 'comment'])
end
end
describe "#diff_match_line" do describe "#diff_match_line" do
let(:old_pos) { 40 } let(:old_pos) { 40 }
let(:new_pos) { 50 } let(:new_pos) { 50 }
......
require 'spec_helper'
describe Gitlab::Utils, lib: true do describe Gitlab::Utils, lib: true do
delegate :to_boolean, to: :described_class delegate :to_boolean, :boolean_to_yes_no, to: :described_class
describe '.to_boolean' do describe '.to_boolean' do
it 'accepts booleans' do it 'accepts booleans' do
...@@ -30,4 +32,11 @@ describe Gitlab::Utils, lib: true do ...@@ -30,4 +32,11 @@ describe Gitlab::Utils, lib: true do
expect(to_boolean(nil)).to be_nil expect(to_boolean(nil)).to be_nil
end end
end end
describe '.boolean_to_yes_no' do
it 'converts booleans to Yes or No' do
expect(boolean_to_yes_no(true)).to eq('Yes')
expect(boolean_to_yes_no(false)).to eq('No')
end
end
end end
# encoding: utf-8
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb')
describe MigrateOldArtifacts do
let(:migration) { described_class.new }
let!(:directory) { Dir.mktmpdir }
before do
allow(Gitlab.config.artifacts).to receive(:path).and_return(directory)
end
after do
FileUtils.remove_entry_secure(directory)
end
context 'with migratable data' do
let(:project1) { create(:empty_project, ci_id: 2) }
let(:project2) { create(:empty_project, ci_id: 3) }
let(:project3) { create(:empty_project) }
let(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
let(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
let(:pipeline3) { create(:ci_empty_pipeline, project: project3) }
let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) }
let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) }
let!(:build2) { create(:ci_build, :artifacts, pipeline: pipeline2) }
let!(:build3) { create(:ci_build, :artifacts, pipeline: pipeline3) }
before do
store_artifacts_in_legacy_path(build_with_legacy_artifacts)
end
it "legacy artifacts are not accessible" do
expect(build_with_legacy_artifacts.artifacts?).to be_falsey
end
it "legacy artifacts are set" do
expect(build_with_legacy_artifacts.artifacts_file_identifier).not_to be_nil
end
describe '#min_id' do
subject { migration.send(:min_id) }
it 'returns the newest build for which ci_id is not defined' do
is_expected.to eq(build3.id)
end
end
describe '#builds_with_artifacts' do
subject { migration.send(:builds_with_artifacts).map(&:id) }
it 'returns a list of builds that has artifacts and could be migrated' do
is_expected.to contain_exactly(build_with_legacy_artifacts.id, build2.id)
end
end
describe '#up' do
context 'when migrating artifacts' do
before do
migration.up
end
it 'all files do have artifacts' do
Ci::Build.with_artifacts do |build|
expect(build).to have_artifacts
end
end
it 'artifacts are no longer present on legacy path' do
expect(File.exist?(legacy_path(build_with_legacy_artifacts))).to eq(false)
end
end
context 'when there are aritfacts in old and new directory' do
before do
store_artifacts_in_legacy_path(build2)
migration.up
end
it 'does not move old files' do
expect(File.exist?(legacy_path(build2))).to eq(true)
end
end
end
private
def store_artifacts_in_legacy_path(build)
FileUtils.mkdir_p(legacy_path(build))
FileUtils.copy(
Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
File.join(legacy_path(build), "ci_build_artifacts.zip"))
FileUtils.copy(
Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
File.join(legacy_path(build), "ci_build_artifacts_metadata.gz"))
build.update_columns(
artifacts_file: 'ci_build_artifacts.zip',
artifacts_metadata: 'ci_build_artifacts_metadata.gz')
build.reload
end
def legacy_path(build)
File.join(directory,
build.created_at.utc.strftime('%Y_%m'),
build.project.ci_id.to_s,
build.id.to_s)
end
end
end
...@@ -1329,16 +1329,49 @@ describe Ci::Build, :models do ...@@ -1329,16 +1329,49 @@ describe Ci::Build, :models do
it { is_expected.to include(tag_variable) } it { is_expected.to include(tag_variable) }
end end
context 'when secure variable is defined' do context 'when secret variable is defined' do
let(:secure_variable) do let(:secret_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false } { key: 'SECRET_KEY', value: 'secret_value', public: false }
end end
before do before do
build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') create(:ci_variable,
secret_variable.slice(:key, :value).merge(project: project))
end end
it { is_expected.to include(secure_variable) } it { is_expected.to include(secret_variable) }
end
context 'when protected variable is defined' do
let(:protected_variable) do
{ key: 'PROTECTED_KEY', value: 'protected_value', public: false }
end
before do
create(:ci_variable,
:protected,
protected_variable.slice(:key, :value).merge(project: project))
end
context 'when the branch is protected' do
before do
create(:protected_branch, project: build.project, name: build.ref)
end
it { is_expected.to include(protected_variable) }
end
context 'when the tag is protected' do
before do
create(:protected_tag, project: build.project, name: build.ref)
end
it { is_expected.to include(protected_variable) }
end
context 'when the ref is not protected' do
it { is_expected.not_to include(protected_variable) }
end
end end
context 'when build is for triggers' do context 'when build is for triggers' do
...@@ -1460,15 +1493,30 @@ describe Ci::Build, :models do ...@@ -1460,15 +1493,30 @@ describe Ci::Build, :models do
end end
context 'returns variables in valid order' do context 'returns variables in valid order' do
let(:build_pre_var) { { key: 'build', value: 'value' } }
let(:project_pre_var) { { key: 'project', value: 'value' } }
let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } }
let(:build_yaml_var) { { key: 'yaml', value: 'value' } }
before do before do
allow(build).to receive(:predefined_variables) { ['predefined'] } allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(project).to receive(:predefined_variables) { ['project'] } allow(project).to receive(:predefined_variables) { [project_pre_var] }
allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] }
allow(build).to receive(:yaml_variables) { ['yaml'] } allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(project).to receive(:secret_variables) { ['secret'] }
allow(project).to receive(:secret_variables_for).with(build.ref) do
[create(:ci_variable, key: 'secret', value: 'value')]
end
end end
it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } it do
is_expected.to eq(
[build_pre_var,
project_pre_var,
pipeline_pre_var,
build_yaml_var,
{ key: 'secret', value: 'value', public: false }])
end
end end
end end
......
...@@ -12,11 +12,33 @@ describe Ci::Variable, models: true do ...@@ -12,11 +12,33 @@ describe Ci::Variable, models: true do
it { is_expected.not_to allow_value('foo bar').for(:key) } it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) } it { is_expected.not_to allow_value('foo/bar').for(:key) }
before :each do describe '.unprotected' do
subject.value = secret_value subject { described_class.unprotected }
context 'when variable is protected' do
before do
create(:ci_variable, :protected)
end
it 'returns nothing' do
is_expected.to be_empty
end
end
context 'when variable is not protected' do
let(:variable) { create(:ci_variable, protected: false) }
it 'returns the variable' do
is_expected.to contain_exactly(variable)
end
end
end end
describe '#value' do describe '#value' do
before do
subject.value = secret_value
end
it 'stores the encrypted value' do it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil expect(subject.encrypted_value).not_to be_nil
end end
...@@ -36,4 +58,11 @@ describe Ci::Variable, models: true do ...@@ -36,4 +58,11 @@ describe Ci::Variable, models: true do
to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end end
end end
describe '#to_runner_variable' do
it 'returns a hash for the runner' do
expect(subject.to_runner_variable)
.to eq(key: subject.key, value: subject.value, public: false)
end
end
end end
...@@ -160,12 +160,6 @@ describe DiffNote, models: true do ...@@ -160,12 +160,6 @@ describe DiffNote, models: true do
context "when noteable is a commit" do context "when noteable is a commit" do
let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) } let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
it "doesn't use the DiffPositionUpdateService" do
expect(Notes::DiffPositionUpdateService).not_to receive(:new)
diff_note
end
it "doesn't update the position" do it "doesn't update the position" do
diff_note diff_note
...@@ -178,12 +172,6 @@ describe DiffNote, models: true do ...@@ -178,12 +172,6 @@ describe DiffNote, models: true do
let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
context "when the note is active" do context "when the note is active" do
it "doesn't use the DiffPositionUpdateService" do
expect(Notes::DiffPositionUpdateService).not_to receive(:new)
diff_note
end
it "doesn't update the position" do it "doesn't update the position" do
diff_note diff_note
...@@ -197,18 +185,11 @@ describe DiffNote, models: true do ...@@ -197,18 +185,11 @@ describe DiffNote, models: true do
allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
end end
it "uses the DiffPositionUpdateService" do it "updates the position" do
service = instance_double("Notes::DiffPositionUpdateService")
expect(Notes::DiffPositionUpdateService).to receive(:new).with(
project,
nil,
old_diff_refs: position.diff_refs,
new_diff_refs: commit.diff_refs,
paths: [path]
).and_return(service)
expect(service).to receive(:execute)
diff_note diff_note
expect(diff_note.original_position).to eq(position)
expect(diff_note.position).not_to eq(position)
end end
end end
end end
......
...@@ -1178,7 +1178,7 @@ describe MergeRequest, models: true do ...@@ -1178,7 +1178,7 @@ describe MergeRequest, models: true do
end end
describe "#reload_diff" do describe "#reload_diff" do
let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) } let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
let(:commit) { subject.project.commit(sample_commit.id) } let(:commit) { subject.project.commit(sample_commit.id) }
...@@ -1197,7 +1197,7 @@ describe MergeRequest, models: true do ...@@ -1197,7 +1197,7 @@ describe MergeRequest, models: true do
subject.reload_diff subject.reload_diff
end end
it "updates diff note positions" do it "updates diff discussion positions" do
old_diff_refs = subject.diff_refs old_diff_refs = subject.diff_refs
# Update merge_request_diff so that #diff_refs will return commit.diff_refs # Update merge_request_diff so that #diff_refs will return commit.diff_refs
...@@ -1211,15 +1211,15 @@ describe MergeRequest, models: true do ...@@ -1211,15 +1211,15 @@ describe MergeRequest, models: true do
subject.merge_request_diff(true) subject.merge_request_diff(true)
end end
expect(Notes::DiffPositionUpdateService).to receive(:new).with( expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
subject.project, subject.project,
subject.author, subject.author,
old_diff_refs: old_diff_refs, old_diff_refs: old_diff_refs,
new_diff_refs: commit.diff_refs, new_diff_refs: commit.diff_refs,
paths: note.position.paths paths: discussion.position.paths
).and_call_original ).and_call_original
expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
expect_any_instance_of(DiffNote).to receive(:save).once expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff(subject.author) subject.reload_diff(subject.author)
......
...@@ -1749,6 +1749,90 @@ describe Project, models: true do ...@@ -1749,6 +1749,90 @@ describe Project, models: true do
end end
end end
describe '#secret_variables_for' do
let(:project) { create(:empty_project) }
let!(:secret_variable) do
create(:ci_variable, value: 'secret', project: project)
end
let!(:protected_variable) do
create(:ci_variable, :protected, value: 'protected', project: project)
end
subject { project.secret_variables_for('ref') }
shared_examples 'ref is protected' do
it 'contains all the variables' do
is_expected.to contain_exactly(secret_variable, protected_variable)
end
end
context 'when the ref is not protected' do
before do
stub_application_setting(
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
it 'contains only the secret variables' do
is_expected.to contain_exactly(secret_variable)
end
end
context 'when the ref is a protected branch' do
before do
create(:protected_branch, name: 'ref', project: project)
end
it_behaves_like 'ref is protected'
end
context 'when the ref is a protected tag' do
before do
create(:protected_tag, name: 'ref', project: project)
end
it_behaves_like 'ref is protected'
end
end
describe '#protected_for?' do
let(:project) { create(:empty_project) }
subject { project.protected_for?('ref') }
context 'when the ref is not protected' do
before do
stub_application_setting(
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when the ref is a protected branch' do
before do
create(:protected_branch, name: 'ref', project: project)
end
it 'returns true' do
is_expected.to be_truthy
end
end
context 'when the ref is a protected tag' do
before do
create(:protected_tag, name: 'ref', project: project)
end
it 'returns true' do
is_expected.to be_truthy
end
end
end
describe '#update_project_statistics' do describe '#update_project_statistics' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
......
...@@ -327,69 +327,114 @@ describe ProjectTeam, models: true do ...@@ -327,69 +327,114 @@ describe ProjectTeam, models: true do
end end
end end
shared_examples_for "#max_member_access_for_users" do |enable_request_store| shared_examples 'max member access for users' do
describe "#max_member_access_for_users" do let(:project) { create(:project) }
before do let(:group) { create(:group) }
RequestStore.begin! if enable_request_store let(:second_group) { create(:group) }
end
after do let(:master) { create(:user) }
if enable_request_store let(:reporter) { create(:user) }
RequestStore.end! let(:guest) { create(:user) }
RequestStore.clear!
end let(:promoted_guest) { create(:user) }
let(:group_developer) { create(:user) }
let(:second_developer) { create(:user) }
let(:user_without_access) { create(:user) }
let(:second_user_without_access) { create(:user) }
let(:users) do
[master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
end end
it 'returns correct roles for different users' do let(:expected) do
master = create(:user) {
reporter = create(:user) master.id => Gitlab::Access::MASTER,
promoted_guest = create(:user) reporter.id => Gitlab::Access::REPORTER,
guest = create(:user) promoted_guest.id => Gitlab::Access::DEVELOPER,
project = create(:empty_project) guest.id => Gitlab::Access::GUEST,
group_developer.id => Gitlab::Access::DEVELOPER,
second_developer.id => Gitlab::Access::MASTER,
user_without_access.id => Gitlab::Access::NO_ACCESS
}
end
before do
project.add_master(master) project.add_master(master)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(promoted_guest) project.add_guest(promoted_guest)
project.add_guest(guest) project.add_guest(guest)
group = create(:group)
group_developer = create(:user)
second_developer = create(:user)
project.project_group_links.create( project.project_group_links.create(
group: group, group: group,
group_access: Gitlab::Access::DEVELOPER) group_access: Gitlab::Access::DEVELOPER
)
group.add_master(promoted_guest) group.add_master(promoted_guest)
group.add_developer(group_developer) group.add_developer(group_developer)
group.add_developer(second_developer) group.add_developer(second_developer)
second_group = create(:group)
project.project_group_links.create( project.project_group_links.create(
group: second_group, group: second_group,
group_access: Gitlab::Access::MASTER) group_access: Gitlab::Access::MASTER
)
second_group.add_master(second_developer) second_group.add_master(second_developer)
end
users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) it 'returns correct roles for different users' do
expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
end
end
expected = { describe '#max_member_access_for_user_ids' do
master.id => Gitlab::Access::MASTER, context 'with RequestStore enabled' do
reporter.id => Gitlab::Access::REPORTER, before do
promoted_guest.id => Gitlab::Access::DEVELOPER, RequestStore.begin!
guest.id => Gitlab::Access::GUEST, end
group_developer.id => Gitlab::Access::DEVELOPER,
second_developer.id => Gitlab::Access::MASTER
}
expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) after do
RequestStore.end!
RequestStore.clear!
end end
include_examples 'max member access for users'
def access_levels(users)
project.team.max_member_access_for_user_ids(users)
end end
it 'does not perform extra queries when asked for users who have already been found' do
access_levels(users)
expect { access_levels(users) }.not_to exceed_query_limit(0)
expect(access_levels(users)).to eq(expected)
end end
describe '#max_member_access_for_users with RequestStore' do it 'only requests the extra users when uncached users are passed' do
it_behaves_like "#max_member_access_for_users", true new_user = create(:user)
second_new_user = create(:user)
all_users = users + [new_user.id, second_new_user.id]
expected_all = expected.merge(new_user.id => Gitlab::Access::NO_ACCESS,
second_new_user.id => Gitlab::Access::NO_ACCESS)
access_levels(users)
queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) }
expect(queries.count).to eq(1)
expect(queries.log_message).to match(/\W#{new_user.id}\W/)
expect(queries.log_message).to match(/\W#{second_new_user.id}\W/)
expect(queries.log_message).not_to match(/\W#{promoted_guest.id}\W/)
expect(access_levels(all_users)).to eq(expected_all)
end
end end
describe '#max_member_access_for_users without RequestStore' do context 'with RequestStore disabled' do
it_behaves_like "#max_member_access_for_users", false include_examples 'max member access for users'
end
end end
end end
...@@ -105,6 +105,15 @@ describe API::V3::DeployKeys do ...@@ -105,6 +105,15 @@ describe API::V3::DeployKeys do
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
end end
it 'accepts can_push parameter' do
key_attrs = attributes_for :write_access_key
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
expect(response).to have_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end end
describe "DELETE /projects/:id/#{path}/:key_id" do describe "DELETE /projects/:id/#{path}/:key_id" do
......
...@@ -42,6 +42,7 @@ describe API::Variables do ...@@ -42,6 +42,7 @@ describe API::Variables do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['value']).to eq(variable.value) expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
end end
it 'responds with 404 Not Found if requesting non-existing variable' do it 'responds with 404 Not Found if requesting non-existing variable' do
...@@ -72,12 +73,13 @@ describe API::Variables do ...@@ -72,12 +73,13 @@ describe API::Variables do
context 'authorized user with proper permissions' do context 'authorized user with proper permissions' do
it 'creates variable' do it 'creates variable' do
expect do expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
end.to change{project.variables.count}.by(1) end.to change{project.variables.count}.by(1)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2') expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2') expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_truthy
end end
it 'does not allow to duplicate variable key' do it 'does not allow to duplicate variable key' do
...@@ -112,13 +114,14 @@ describe API::Variables do ...@@ -112,13 +114,14 @@ describe API::Variables do
initial_variable = project.variables.first initial_variable = project.variables.first
value_before = initial_variable.value value_before = initial_variable.value
put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP' put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
updated_variable = project.variables.first updated_variable = project.variables.first
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(value_before).to eq(variable.value) expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP') expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
end end
it 'responds with 404 Not Found if requesting non-existing variable' do it 'responds with 404 Not Found if requesting non-existing variable' do
......
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/activerecord_serialize'
describe RuboCop::Cop::ActiverecordSerialize do
include CopHelper
subject(:cop) { described_class.new }
context 'inside the app/models directory' do
it 'registers an offense when serialize is used' do
allow(cop).to receive(:in_models?).and_return(true)
inspect_source(cop, 'serialize :foo')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
end
end
end
context 'outside the app/models directory' do
it 'does nothing' do
allow(cop).to receive(:in_models?).and_return(false)
inspect_source(cop, 'serialize :foo')
expect(cop.offenses).to be_empty
end
end
end
require 'spec_helper' require 'spec_helper'
describe Notes::DiffPositionUpdateService, services: true do describe Discussions::UpdateDiffPositionService, services: true do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:current_user) { project.owner } let(:current_user) { project.owner }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
...@@ -138,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do ...@@ -138,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do
# .. .. # .. ..
describe "#execute" do describe "#execute" do
let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion }
let(:old_position) do let(:old_position) do
Gitlab::Diff::Position.new( Gitlab::Diff::Position.new(
...@@ -154,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do ...@@ -154,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 16 } let(:line) { 16 }
it "updates the position" do it "updates the position" do
subject.execute(note) subject.execute(discussion)
expect(note.original_position).to eq(old_position) expect(discussion.original_position).to eq(old_position)
expect(note.position).not_to eq(old_position) expect(discussion.position).not_to eq(old_position)
expect(note.position.new_line).to eq(22) expect(discussion.position.new_line).to eq(22)
end end
end end
...@@ -166,27 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do ...@@ -166,27 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 9 } let(:line) { 9 }
it "doesn't update the position" do it "doesn't update the position" do
subject.execute(note) subject.execute(discussion)
expect(note.original_position).to eq(old_position) expect(discussion.original_position).to eq(old_position)
expect(note.position).to eq(old_position) expect(discussion.position).to eq(old_position)
end end
it 'sets the change position' do it 'sets the change position' do
subject.execute(note) subject.execute(discussion)
change_position = note.change_position change_position = discussion.change_position
expect(change_position.start_sha).to eq(old_diff_refs.head_sha) expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
expect(change_position.head_sha).to eq(new_diff_refs.head_sha) expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
expect(change_position.old_line).to eq(9) expect(change_position.old_line).to eq(9)
expect(change_position.new_line).to be_nil expect(change_position.new_line).to be_nil
end end
it 'creates a system note' do it 'creates a system discussion' do
expect(SystemNoteService).to receive(:diff_discussion_outdated).with( expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
note.to_discussion, project, current_user, instance_of(Gitlab::Diff::Position)) discussion, project, current_user, instance_of(Gitlab::Diff::Position))
subject.execute(note) subject.execute(discussion)
end end
end end
end end
......
require 'spec_helper'
describe GravatarService, service: true do
describe '#execute' do
let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' }
before do
allow(Gitlab.config.gravatar).to receive(:plain_url).and_return(url)
end
it 'replaces the placeholders' do
avatar_url = described_class.new.execute('user@example.com', 100, 2, username: 'user')
expect(avatar_url).to include("hash=#{Digest::MD5.hexdigest('user@example.com')}")
expect(avatar_url).to include("size=200")
expect(avatar_url).to include("email=user%40example.com")
expect(avatar_url).to include("username=user")
end
end
end
require 'rails_helper'
describe ArtifactUploader do
let(:job) { create(:ci_build) }
let(:uploader) { described_class.new(job, :artifacts_file) }
let(:path) { Gitlab.config.artifacts.path }
describe '.local_artifacts_store' do
subject { described_class.local_artifacts_store }
it "delegate to artifacts path" do
expect(Gitlab.config.artifacts).to receive(:path)
subject
end
end
describe '.artifacts_upload_path' do
subject { described_class.artifacts_upload_path }
it { is_expected.to start_with(path) }
it { is_expected.to end_with('tmp/uploads/') }
end
describe '#store_dir' do
subject { uploader.store_dir }
it { is_expected.to start_with(path) }
it { is_expected.to end_with("#{job.project_id}/#{job.id}") }
end
describe '#cache_dir' do
subject { uploader.cache_dir }
it { is_expected.to start_with(path) }
it { is_expected.to end_with('tmp/cache') }
end
end
require 'rails_helper'
require 'carrierwave/storage/fog'
describe GitlabUploader do
let(:uploader_class) { Class.new(described_class) }
subject { uploader_class.new }
describe '#file_storage?' do
context 'when file storage is used' do
before do
uploader_class.storage(:file)
end
it { is_expected.to be_file_storage }
end
context 'when is remote storage' do
before do
uploader_class.storage(:fog)
end
it { is_expected.not_to be_file_storage }
end
end
describe '#file_cache_storage?' do
context 'when file storage is used' do
before do
uploader_class.cache_storage(:file)
end
it { is_expected.to be_file_cache_storage }
end
context 'when is remote storage' do
before do
uploader_class.cache_storage(:fog)
end
it { is_expected.not_to be_file_cache_storage }
end
end
describe '#move_to_cache' do
it 'is true' do
expect(subject.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
expect(subject.move_to_store).to eq(true)
end
end
end
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