Commit 84727c82 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d2798d60
## Problem Statement
<!-- What is the problem we hope to validate and solve? -->
## Reach
<!-- Please describe who suffers from this problem. Consider referring to our personas, which are described at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
<!-- Please also quantify the problem's reach using the following values, considering an aggregate across GitLab.com and self-managed:
10.0 = Impacts the vast majority (~80% or greater) of our users, prospects, or customers.
6.0 = Impacts a large percentage (~50% to ~80%) of the above.
3.0 = Significant reach (~25% to ~50%).
1.5 = Small reach (~5% to ~25%).
0.5 = Minimal reach (Less than ~5%). -->
## Impact
<!-- How do we positively impact the users above and GitLab's business by solving this problem? Please describe briefly, and provide a numerical assessment:
3.0 = Massive impact
2.0 = High impact
1.0 = Medium impact
0.5 = Low impact
0.25 = Minimal impact -->
## Confidence
<!-- How do we know this is a problem? Please provide and link to any supporting information (e.g. data, customer verbatims) and use this basis to provide a numerical assessment on our confidence level in this problem's severity:
100% = High confidence
80% = Medium confidence
50% = Low confidence -->
## Effort
<!-- How much effort do we think it will be to solve this problem? Please include all counterparts (Product, UX, Engineering, etc) in your assessment and quantify the number of person-months needed to dedicate to the effort.
For example, if the solution will take a product manager, designer, and engineer two weeks of effort - you may quantify this as 1.5 (based on 0.5 months x 3 people). -->
/label ~"workflow::problem backlog"
...@@ -39,10 +39,6 @@ ...@@ -39,10 +39,6 @@
min-height: $header-height; min-height: $header-height;
} }
.snippet-edited-ago {
color: $gray-darkest;
}
.snippet-actions { .snippet-actions {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
float: right; float: right;
......
...@@ -211,7 +211,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -211,7 +211,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
def discussions def discussions
merge_request.preload_discussions_diff_highlight merge_request.discussions_diffs.load_highlight
super super
end end
......
...@@ -108,13 +108,10 @@ class DiffNote < Note ...@@ -108,13 +108,10 @@ class DiffNote < Note
end end
def fetch_diff_file def fetch_diff_file
return note_diff_file.raw_diff_file if note_diff_file
file = file =
if note_diff_file if created_at_diff?(noteable.diff_refs)
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
repository: repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're # We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff. # presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it. # So no need to make an extra Gitaly diff request for it.
...@@ -126,9 +123,7 @@ class DiffNote < Note ...@@ -126,9 +123,7 @@ class DiffNote < Note
original_position.diff_file(repository) original_position.diff_file(repository)
end end
# Since persisted diff files already have its content "unfolded" file&.unfold_diff_lines(position)
# there's no need to make it pass through the unfolding process.
file&.unfold_diff_lines(position) unless note_diff_file
file file
end end
......
...@@ -454,24 +454,17 @@ class MergeRequest < ApplicationRecord ...@@ -454,24 +454,17 @@ class MergeRequest < ApplicationRecord
true true
end end
def preload_discussions_diff_highlight
preloadable_files = note_diff_files.for_commit_or_unresolved
discussions_diffs.load_highlight(preloadable_files.pluck(:id))
end
def discussions_diffs def discussions_diffs
strong_memoize(:discussions_diffs) do strong_memoize(:discussions_diffs) do
note_diff_files = NoteDiffFile
.joins(:diff_note)
.merge(notes.or(commit_notes))
.includes(diff_note: :project)
Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a) Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
end end
end end
def note_diff_files
NoteDiffFile
.where(diff_note: discussion_notes)
.includes(diff_note: :project)
end
def diff_size def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform # Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here. # highlighting, which we don't need here.
......
...@@ -27,11 +27,8 @@ class Milestone < ApplicationRecord ...@@ -27,11 +27,8 @@ class Milestone < ApplicationRecord
belongs_to :project belongs_to :project
belongs_to :group belongs_to :group
# A one-to-one relationship is set up here as part of a MVC: https://gitlab.com/gitlab-org/gitlab-ce/issues/62402 has_many :milestone_releases
# However, on the long term, we will want a many-to-many relationship between Release and Milestone. has_many :releases, through: :milestone_releases
# The "has_one through" allows us today to set up this one-to-one relationship while setting up the architecture for the long-term (ie intermediate table).
has_one :milestone_release
has_one :release, through: :milestone_release
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
...@@ -68,7 +65,7 @@ class Milestone < ApplicationRecord ...@@ -68,7 +65,7 @@ class Milestone < ApplicationRecord
validate :milestone_type_check validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits validate :dates_within_4_digits
validates_associated :milestone_release, message: -> (_, obj) { obj[:value].errors.full_messages.join(",") } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
strip_attributes :title strip_attributes :title
......
...@@ -4,9 +4,11 @@ class MilestoneRelease < ApplicationRecord ...@@ -4,9 +4,11 @@ class MilestoneRelease < ApplicationRecord
belongs_to :milestone belongs_to :milestone
belongs_to :release belongs_to :release
validates :milestone_id, uniqueness: { scope: [:release_id] }
validate :same_project_between_milestone_and_release validate :same_project_between_milestone_and_release
# Keep until 2019-11-29
self.ignored_columns += %i[id]
private private
def same_project_between_milestone_and_release def same_project_between_milestone_and_release
......
...@@ -3,15 +3,11 @@ ...@@ -3,15 +3,11 @@
class NoteDiffFile < ApplicationRecord class NoteDiffFile < ApplicationRecord
include DiffFile include DiffFile
scope :for_commit_or_unresolved, -> do
joins(:diff_note).where("resolved_at IS NULL OR noteable_type = 'Commit'")
end
scope :referencing_sha, -> (oids, project_id:) do scope :referencing_sha, -> (oids, project_id:) do
joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids }) joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids })
end end
delegate :original_position, :project, to: :diff_note delegate :original_position, :project, :resolved_at, to: :diff_note
belongs_to :diff_note, inverse_of: :note_diff_file belongs_to :diff_note, inverse_of: :note_diff_file
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class BugzillaService < IssueTrackerService class BugzillaService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :project_url, :issues_url, :new_issue_url
def default_title def default_title
'Bugzilla' 'Bugzilla'
end end
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class CustomIssueTrackerService < IssueTrackerService class CustomIssueTrackerService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def default_title def default_title
'Custom Issue Tracker' 'Custom Issue Tracker'
end end
......
...@@ -3,8 +3,56 @@ ...@@ -3,8 +3,56 @@
module DataFields module DataFields
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do
# Provide convenient accessor methods for data fields.
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
def data_field(*args)
args.each do |arg|
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
unless method_defined?(arg)
def #{arg}
data_fields.send('#{arg}') || (properties && properties['#{arg}'])
end
end
def #{arg}=(value)
@old_data_fields ||= {}
@old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only
data_fields.send('#{arg}=', value)
end
def #{arg}_touched?
@old_data_fields ||= {}
@old_data_fields.has_key?('#{arg}')
end
def #{arg}_changed?
#{arg}_touched? && @old_data_fields['#{arg}'] != #{arg}
end
def #{arg}_was
return unless #{arg}_touched?
return if data_fields.persisted? # arg_was does not work for attr_encrypted
legacy_properties_data['#{arg}']
end
RUBY
end
end
end
included do included do
has_one :issue_tracker_data has_one :issue_tracker_data, autosave: true
has_one :jira_tracker_data has_one :jira_tracker_data, autosave: true
def data_fields
raise NotImplementedError
end
def data_fields_present?
data_fields.persisted?
rescue NotImplementedError
false
end
end end
end end
...@@ -5,8 +5,6 @@ class GitlabIssueTrackerService < IssueTrackerService ...@@ -5,8 +5,6 @@ class GitlabIssueTrackerService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :project_url, :issues_url, :new_issue_url
default_value_for :default, true default_value_for :default, true
def default_title def default_title
......
...@@ -6,9 +6,6 @@ class IssueTrackerData < ApplicationRecord ...@@ -6,9 +6,6 @@ class IssueTrackerData < ApplicationRecord
delegate :activated?, to: :service, allow_nil: true delegate :activated?, to: :service, allow_nil: true
validates :service, presence: true validates :service, presence: true
validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated?
def self.encryption_options def self.encryption_options
{ {
......
...@@ -3,9 +3,14 @@ ...@@ -3,9 +3,14 @@
class IssueTrackerService < Service class IssueTrackerService < Service
validate :one_issue_tracker, if: :activated?, on: :manual_change validate :one_issue_tracker, if: :activated?, on: :manual_change
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
data_field :project_url, :issues_url, :new_issue_url
default_value_for :category, 'issue_tracker' default_value_for :category, 'issue_tracker'
before_save :handle_properties before_validation :handle_properties
before_validation :set_default_data, on: :create
# Pattern used to extract links from comments # Pattern used to extract links from comments
# Override this method on services that uses different patterns # Override this method on services that uses different patterns
...@@ -43,12 +48,31 @@ class IssueTrackerService < Service ...@@ -43,12 +48,31 @@ class IssueTrackerService < Service
end end
def handle_properties def handle_properties
properties.slice('title', 'description').each do |key, _| # this has been moved from initialize_properties and should be improved
# as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
return unless properties
@legacy_properties_data = properties.dup
data_values = properties.slice!('title', 'description')
properties.each do |key, _|
current_value = self.properties.delete(key) current_value = self.properties.delete(key)
value = attribute_changed?(key) ? attribute_change(key).last : current_value value = attribute_changed?(key) ? attribute_change(key).last : current_value
write_attribute(key, value) write_attribute(key, value)
end end
data_values.reject! { |key| data_fields.changed.include?(key) }
data_fields.assign_attributes(data_values) if data_values.present?
self.properties = {}
end
def legacy_properties_data
@legacy_properties_data ||= {}
end
def data_fields
issue_tracker_data || self.build_issue_tracker_data
end end
def default? def default?
...@@ -56,7 +80,7 @@ class IssueTrackerService < Service ...@@ -56,7 +80,7 @@ class IssueTrackerService < Service
end end
def issue_url(iid) def issue_url(iid)
self.issues_url.gsub(':id', iid.to_s) issues_url.gsub(':id', iid.to_s)
end end
def issue_tracker_path def issue_tracker_path
...@@ -80,25 +104,22 @@ class IssueTrackerService < Service ...@@ -80,25 +104,22 @@ class IssueTrackerService < Service
] ]
end end
def initialize_properties
{}
end
# Initialize with default properties values # Initialize with default properties values
# or receive a block with custom properties def set_default_data
def initialize_properties(&block) return unless issues_tracker.present?
return unless properties.nil?
self.title ||= issues_tracker['title']
if enabled_in_gitlab_config
if block_given? # we don't want to override if we have set something
yield return if project_url || issues_url || new_issue_url
else
self.properties = { data_fields.project_url = issues_tracker['project_url']
title: issues_tracker['title'], data_fields.issues_url = issues_tracker['issues_url']
project_url: issues_tracker['project_url'], data_fields.new_issue_url = issues_tracker['new_issue_url']
issues_url: issues_tracker['issues_url'],
new_issue_url: issues_tracker['new_issue_url']
}
end
else
self.properties = {}
end
end end
def self.supported_events def self.supported_events
......
...@@ -17,7 +17,10 @@ class JiraService < IssueTrackerService ...@@ -17,7 +17,10 @@ class JiraService < IssueTrackerService
# Jira Cloud version is deprecating authentication via username and password. # Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud, # We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936. # for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
data_field :username, :password, :url, :api_url, :jira_issue_transition_id
before_update :reset_password before_update :reset_password
...@@ -35,24 +38,34 @@ class JiraService < IssueTrackerService ...@@ -35,24 +38,34 @@ class JiraService < IssueTrackerService
end end
def initialize_properties def initialize_properties
super do {}
self.properties = { end
url: issues_tracker['url'],
api_url: issues_tracker['api_url'] def data_fields
} jira_tracker_data || self.build_jira_tracker_data
end
end end
def reset_password def reset_password
self.password = nil if reset_password? data_fields.password = nil if reset_password?
end
def set_default_data
return unless issues_tracker.present?
self.title ||= issues_tracker['title']
return if url
data_fields.url ||= issues_tracker['url']
data_fields.api_url ||= issues_tracker['api_url']
end end
def options def options
url = URI.parse(client_url) url = URI.parse(client_url)
{ {
username: self.username, username: username,
password: self.password, password: password,
site: URI.join(url, '/').to_s, # Intended to find the root site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path, context_path: url.path,
auth_type: :basic, auth_type: :basic,
......
...@@ -6,13 +6,6 @@ class JiraTrackerData < ApplicationRecord ...@@ -6,13 +6,6 @@ class JiraTrackerData < ApplicationRecord
delegate :activated?, to: :service, allow_nil: true delegate :activated?, to: :service, allow_nil: true
validates :service, presence: true validates :service, presence: true
validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated?
validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
validates :jira_issue_transition_id,
format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") },
allow_blank: true
def self.encryption_options def self.encryption_options
{ {
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class RedmineService < IssueTrackerService class RedmineService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :project_url, :issues_url, :new_issue_url
def default_title def default_title
'Redmine' 'Redmine'
end end
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class YoutrackService < IssueTrackerService class YoutrackService < IssueTrackerService
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
prop_accessor :project_url, :issues_url
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
def self.reference_pattern(only_long: false) def self.reference_pattern(only_long: false)
if only_long if only_long
......
...@@ -12,11 +12,8 @@ class Release < ApplicationRecord ...@@ -12,11 +12,8 @@ class Release < ApplicationRecord
has_many :links, class_name: 'Releases::Link' has_many :links, class_name: 'Releases::Link'
# A one-to-one relationship is set up here as part of a MVC: https://gitlab.com/gitlab-org/gitlab-ce/issues/62402 has_many :milestone_releases
# However, on the long term, we will want a many-to-many relationship between Release and Milestone. has_many :milestones, through: :milestone_releases
# The "has_one through" allows us today to set up this one-to-one relationship while setting up the architecture for the long-term (ie intermediate table).
has_one :milestone_release
has_one :milestone, through: :milestone_release
default_value_for :released_at, allows_nil: false do default_value_for :released_at, allows_nil: false do
Time.zone.now Time.zone.now
...@@ -26,7 +23,7 @@ class Release < ApplicationRecord ...@@ -26,7 +23,7 @@ class Release < ApplicationRecord
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create validates :name, presence: true, on: :create
validates_associated :milestone_release, message: -> (_, obj) { obj[:value].errors.full_messages.join(",") } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) } scope :sorted, -> { order(released_at: :desc) }
......
...@@ -48,25 +48,29 @@ module Releases ...@@ -48,25 +48,29 @@ module Releases
end end
end end
def milestone def milestones
return unless params[:milestone] return [] unless param_for_milestone_titles_provided?
strong_memoize(:milestone) do strong_memoize(:milestones) do
MilestonesFinder.new( MilestonesFinder.new(
project: project, project: project,
current_user: current_user, current_user: current_user,
project_ids: Array(project.id), project_ids: Array(project.id),
title: params[:milestone] state: 'all',
).execute.first title: params[:milestones]
).execute
end end
end end
def inexistent_milestone? def inexistent_milestones
params[:milestone] && !params[:milestone].empty? && !milestone return [] unless param_for_milestone_titles_provided?
existing_milestone_titles = milestones.map(&:title)
Array(params[:milestones]) - existing_milestone_titles
end end
def param_for_milestone_title_provided? def param_for_milestone_titles_provided?
params[:milestone].present? || params[:milestone]&.empty? params.key?(:milestones)
end end
end end
end end
......
...@@ -7,7 +7,7 @@ module Releases ...@@ -7,7 +7,7 @@ module Releases
def execute def execute
return error('Access Denied', 403) unless allowed? return error('Access Denied', 403) unless allowed?
return error('Release already exists', 409) if release return error('Release already exists', 409) if release
return error('Milestone does not exist', 400) if inexistent_milestone? return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
tag = ensure_tag tag = ensure_tag
...@@ -61,7 +61,7 @@ module Releases ...@@ -61,7 +61,7 @@ module Releases
sha: tag.dereferenced_target.sha, sha: tag.dereferenced_target.sha,
released_at: released_at, released_at: released_at,
links_attributes: params.dig(:assets, 'links') || [], links_attributes: params.dig(:assets, 'links') || [],
milestone: milestone milestones: milestones
) )
end end
end end
......
...@@ -9,9 +9,9 @@ module Releases ...@@ -9,9 +9,9 @@ module Releases
return error('Release does not exist', 404) unless release return error('Release does not exist', 404) unless release
return error('Access Denied', 403) unless allowed? return error('Access Denied', 403) unless allowed?
return error('params is empty', 400) if empty_params? return error('params is empty', 400) if empty_params?
return error('Milestone does not exist', 400) if inexistent_milestone? return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
params[:milestone] = milestone if param_for_milestone_title_provided? params[:milestones] = milestones if param_for_milestone_titles_provided?
if release.update(params) if release.update(params)
success(tag: existing_tag, release: release) success(tag: existing_tag, release: release)
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= f.label :file_name, "File" = f.label :file_name, "File"
.col-sm-10 .col-sm-10
.file-holder.snippet .file-holder.snippet
.js-file-title.file-title .js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name' = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name'
.file-content.code .file-content.code
%pre#editor= @snippet.content %pre#editor= @snippet.content
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
= @snippet.description = @snippet.description
- if @snippet.updated_at != @snippet.created_at - if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', exclude_author: true)
- if @snippet.embeddable? - if @snippet.embeddable?
.embed-snippet .embed-snippet
......
---
title: Switch Milestone and Release to a many-to-many relationship
merge_request: 16517
author:
type: changed
---
title: Database table for tracking programming language trends over time
merge_request: 16491
author:
type: added
---
title: Considerably improve the query performance for MR discussions load
merge_request: 16635
author:
type: performance
...@@ -117,7 +117,7 @@ Rails.application.routes.draw do ...@@ -117,7 +117,7 @@ Rails.application.routes.draw do
end end
Gitlab.ee do Gitlab.ee do
constraints(::Constraints::FeatureConstrainer.new(:analytics)) do constraints(-> (*) { Gitlab::Analytics.any_features_enabled? }) do
draw :analytics draw :analytics
end end
end end
......
# frozen_string_literal: true
class CreateAnalyticsLanguageTrendRepositoryLanguages < ActiveRecord::Migration[5.2]
DOWNTIME = false
INDEX_PREFIX = 'analytics_repository_languages_'
def change
create_table :analytics_language_trend_repository_languages, id: false do |t|
t.integer :file_count, null: false, default: 0
t.references :programming_language, {
null: false,
foreign_key: { on_delete: :cascade },
index: false
}
t.references :project, {
null: false,
foreign_key: { on_delete: :cascade },
index: { name: INDEX_PREFIX + 'on_project_id' }
}
t.integer :loc, null: false, default: 0
t.integer :bytes, null: false, default: 0
# Storing percentage (with 2 decimal places), on 2 bytes.
# 50.25% => 5025
# Max: 100.00% => 10000 (fits smallint: 32767)
t.integer :percentage, limit: 2, null: false, default: 0
t.date :snapshot_date, null: false
end
add_index :analytics_language_trend_repository_languages, %I[
programming_language_id
project_id
snapshot_date
], name: INDEX_PREFIX + 'unique_index', unique: true
end
end
# frozen_string_literal: true
class RemoveIdColumnFromIntermediateReleaseMilestones < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
remove_column :milestone_releases, :id, :bigint
end
end
...@@ -81,6 +81,18 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do ...@@ -81,6 +81,18 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do
t.index ["start_event_label_id"], name: "index_analytics_ca_project_stages_on_start_event_label_id" t.index ["start_event_label_id"], name: "index_analytics_ca_project_stages_on_start_event_label_id"
end end
create_table "analytics_language_trend_repository_languages", id: false, force: :cascade do |t|
t.integer "file_count", default: 0, null: false
t.bigint "programming_language_id", null: false
t.bigint "project_id", null: false
t.integer "loc", default: 0, null: false
t.integer "bytes", default: 0, null: false
t.integer "percentage", limit: 2, default: 0, null: false
t.date "snapshot_date", null: false
t.index ["programming_language_id", "project_id", "snapshot_date"], name: "analytics_repository_languages_unique_index", unique: true
t.index ["project_id"], name: "analytics_repository_languages_on_project_id"
end
create_table "appearances", id: :serial, force: :cascade do |t| create_table "appearances", id: :serial, force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
t.text "description", null: false t.text "description", null: false
...@@ -2196,7 +2208,7 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do ...@@ -2196,7 +2208,7 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do
t.index ["user_id"], name: "index_merge_trains_on_user_id" t.index ["user_id"], name: "index_merge_trains_on_user_id"
end end
create_table "milestone_releases", force: :cascade do |t| create_table "milestone_releases", id: false, force: :cascade do |t|
t.bigint "milestone_id", null: false t.bigint "milestone_id", null: false
t.bigint "release_id", null: false t.bigint "release_id", null: false
t.index ["milestone_id", "release_id"], name: "index_miletone_releases_on_milestone_and_release", unique: true t.index ["milestone_id", "release_id"], name: "index_miletone_releases_on_milestone_and_release", unique: true
...@@ -3762,6 +3774,8 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do ...@@ -3762,6 +3774,8 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do
add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "end_event_label_id", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "end_event_label_id", on_delete: :cascade
add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "start_event_label_id", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "start_event_label_id", on_delete: :cascade
add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "programming_languages", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "projects", on_delete: :cascade
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "instance_administration_project_id", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "instance_administration_project_id", on_delete: :nullify
......
This diff is collapsed.
...@@ -92,5 +92,18 @@ For instance: ...@@ -92,5 +92,18 @@ For instance:
Model.create(foo: params[:foo]) Model.create(foo: params[:foo])
``` ```
## Using API path helpers in GitLab Rails codebase
Because we support [installing GitLab under a relative URL], one must take this
into account when using API path helpers generated by Grape. Any such API path
helper usage must be in wrapped into the `expose_path` helper call.
For instance:
```haml
- endpoint = expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid))
```
[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb [Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb
[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion [validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
[installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html
...@@ -16,6 +16,11 @@ sudo gitlab-rake gitlab:cleanup:dirs ...@@ -16,6 +16,11 @@ sudo gitlab-rake gitlab:cleanup:dirs
bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production
``` ```
DANGER: **Danger:**
The following task does not currently work as expected.
The use will probably mark more existing repositories as orphaned.
For more information, see the [issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/24633).
Rename repositories from all repository storage paths if they don't exist in GitLab database. Rename repositories from all repository storage paths if they don't exist in GitLab database.
The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created. The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created.
......
...@@ -1044,7 +1044,12 @@ module API ...@@ -1044,7 +1044,12 @@ module API
expose :job_events expose :job_events
# Expose serialized properties # Expose serialized properties
expose :properties do |service, options| expose :properties do |service, options|
service.properties.slice(*service.api_field_names) # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
if service.data_fields_present?
service.data_fields.as_json.slice(*service.api_field_names)
else
service.properties.slice(*service.api_field_names)
end
end end
end end
...@@ -1280,7 +1285,7 @@ module API ...@@ -1280,7 +1285,7 @@ module API
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
expose :upcoming_release?, as: :upcoming_release expose :upcoming_release?, as: :upcoming_release
expose :milestone, using: Entities::Milestone, if: -> (release, _) { release.milestone.present? } expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
expose :assets do expose :assets do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
......
...@@ -54,7 +54,7 @@ module API ...@@ -54,7 +54,7 @@ module API
requires :url, type: String requires :url, type: String
end end
end end
optional :milestone, type: String, desc: 'The title of the related milestone' optional :milestones, type: Array, desc: 'The titles of the related milestones', default: []
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.' optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end end
post ':id/releases' do post ':id/releases' do
...@@ -80,7 +80,7 @@ module API ...@@ -80,7 +80,7 @@ module API
optional :name, type: String, desc: 'The name of the release' optional :name, type: String, desc: 'The name of the release'
optional :description, type: String, desc: 'Release notes with markdown support' optional :description, type: String, desc: 'Release notes with markdown support'
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready.' optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready.'
optional :milestone, type: String, desc: 'The title of the related milestone' optional :milestones, type: Array, desc: 'The titles of the related milestones'
end end
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
authorize_update_release! authorize_update_release!
......
...@@ -4,11 +4,16 @@ module Gitlab ...@@ -4,11 +4,16 @@ module Gitlab
module DiscussionsDiff module DiscussionsDiff
class FileCollection class FileCollection
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include Enumerable
def initialize(collection) def initialize(collection)
@collection = collection @collection = collection
end end
def each(&block)
@collection.each(&block)
end
# Returns a Gitlab::Diff::File with the given ID (`unique_identifier` in # Returns a Gitlab::Diff::File with the given ID (`unique_identifier` in
# Gitlab::Diff::File). # Gitlab::Diff::File).
def find_by_id(id) def find_by_id(id)
...@@ -16,20 +21,12 @@ module Gitlab ...@@ -16,20 +21,12 @@ module Gitlab
end end
# Writes cache and preloads highlighted diff lines for # Writes cache and preloads highlighted diff lines for
# object IDs, in @collection. # highlightable object IDs, in @collection.
#
# highlightable_ids - Diff file `Array` responding to ID. The ID will be used
# to generate the cache key.
# #
# - Highlight cache is written just for uncached diff files # - Highlight cache is written just for uncached diff files
# - The cache content is not updated (there's no need to do so) # - The cache content is not updated (there's no need to do so)
def load_highlight(highlightable_ids) def load_highlight
preload_highlighted_lines(highlightable_ids) ids = highlightable_collection_ids
end
private
def preload_highlighted_lines(ids)
cached_content = read_cache(ids) cached_content = read_cache(ids)
uncached_ids = ids.select.each_with_index { |_, i| cached_content[i].nil? } uncached_ids = ids.select.each_with_index { |_, i| cached_content[i].nil? }
...@@ -46,6 +43,12 @@ module Gitlab ...@@ -46,6 +43,12 @@ module Gitlab
end end
end end
private
def highlightable_collection_ids
each.with_object([]) { |file, memo| memo << file.id unless file.resolved_at }
end
def read_cache(ids) def read_cache(ids)
HighlightCache.read_multiple(ids) HighlightCache.read_multiple(ids)
end end
...@@ -57,9 +60,7 @@ module Gitlab ...@@ -57,9 +60,7 @@ module Gitlab
end end
def diff_files def diff_files
strong_memoize(:diff_files) do strong_memoize(:diff_files) { map(&:raw_diff_file) }
@collection.map(&:raw_diff_file)
end
end end
# Processes the diff lines highlighting for diff files matching the given # Processes the diff lines highlighting for diff files matching the given
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Gitlab module Gitlab
class UsageData class UsageData
APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze
BATCH_SIZE = 100
class << self class << self
def data(force_refresh: false) def data(force_refresh: false)
...@@ -13,10 +14,10 @@ module Gitlab ...@@ -13,10 +14,10 @@ module Gitlab
def uncached_data def uncached_data
license_usage_data.merge(system_usage_data) license_usage_data.merge(system_usage_data)
.merge(features_usage_data) .merge(features_usage_data)
.merge(components_usage_data) .merge(components_usage_data)
.merge(cycle_analytics_usage_data) .merge(cycle_analytics_usage_data)
.merge(usage_counters) .merge(usage_counters)
end end
def to_json(force_refresh: false) def to_json(force_refresh: false)
...@@ -96,9 +97,8 @@ module Gitlab ...@@ -96,9 +97,8 @@ module Gitlab
todos: count(Todo), todos: count(Todo),
uploads: count(Upload), uploads: count(Upload),
web_hooks: count(WebHook) web_hooks: count(WebHook)
} }.merge(services_usage)
.merge(services_usage) .merge(approximate_counts)
.merge(approximate_counts)
}.tap do |data| }.tap do |data|
data[:counts][:user_preferences] = user_preferences_usage data[:counts][:user_preferences] = user_preferences_usage
end end
...@@ -173,17 +173,34 @@ module Gitlab ...@@ -173,17 +173,34 @@ module Gitlab
def jira_usage def jira_usage
# Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999 # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
# so we can just check for subdomains of atlassian.net # so we can just check for subdomains of atlassian.net
services = count(
Service.unscoped.where(type: :JiraService, active: true)
.group("CASE WHEN properties LIKE '%.atlassian.net%' THEN 'cloud' ELSE 'server' END"),
fallback: Hash.new(-1)
)
{ results = {
projects_jira_server_active: services['server'] || 0, projects_jira_server_active: 0,
projects_jira_cloud_active: services['cloud'] || 0, projects_jira_cloud_active: 0,
projects_jira_active: services['server'] == -1 ? -1 : services.values.sum projects_jira_active: -1
} }
Service.unscoped
.where(type: :JiraService, active: true)
.includes(:jira_tracker_data)
.find_in_batches(batch_size: BATCH_SIZE) do |services|
counts = services.group_by do |service|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
service_url&.include?('.atlassian.net') ? :cloud : :server
end
results[:projects_jira_server_active] += counts[:server].count if counts[:server]
results[:projects_jira_cloud_active] += counts[:cloud].count if counts[:cloud]
if results[:projects_jira_active] == -1
results[:projects_jira_active] = count(services)
else
results[:projects_jira_active] += count(services)
end
end
results
end end
def user_preferences_usage def user_preferences_usage
......
...@@ -1289,7 +1289,7 @@ describe Projects::MergeRequestsController do ...@@ -1289,7 +1289,7 @@ describe Projects::MergeRequestsController do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
note_diff_file = commit_diff_note.note_diff_file note_diff_file = commit_diff_note.note_diff_file
expect(collection).to receive(:load_highlight).with([note_diff_file.id]).and_call_original expect(collection).to receive(:load_highlight).and_call_original
expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
end end
...@@ -1306,7 +1306,7 @@ describe Projects::MergeRequestsController do ...@@ -1306,7 +1306,7 @@ describe Projects::MergeRequestsController do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
note_diff_file = diff_note.note_diff_file note_diff_file = diff_note.note_diff_file
expect(collection).to receive(:load_highlight).with([note_diff_file.id]).and_call_original expect(collection).to receive(:load_highlight).and_call_original
expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
end end
...@@ -1319,7 +1319,7 @@ describe Projects::MergeRequestsController do ...@@ -1319,7 +1319,7 @@ describe Projects::MergeRequestsController do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
note_diff_file = diff_note.note_diff_file note_diff_file = diff_note.note_diff_file
expect(collection).to receive(:load_highlight).with([]).and_call_original expect(collection).to receive(:load_highlight).and_call_original
expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
end end
......
...@@ -9,11 +9,7 @@ FactoryBot.define do ...@@ -9,11 +9,7 @@ FactoryBot.define do
factory :custom_issue_tracker_service, class: CustomIssueTrackerService do factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
project project
active true active true
properties( issue_tracker
project_url: 'https://project.url.com',
issues_url: 'https://issues.url.com',
new_issue_url: 'https://newissue.url.com'
)
end end
factory :emails_on_push_service do factory :emails_on_push_service do
...@@ -47,12 +43,24 @@ FactoryBot.define do ...@@ -47,12 +43,24 @@ FactoryBot.define do
factory :jira_service do factory :jira_service do
project project
active true active true
properties(
url: 'https://jira.example.com', transient do
username: 'jira_user', create_data true
password: 'my-secret-password', url 'https://jira.example.com'
project_key: 'jira-key' api_url nil
) username 'jira_username'
password 'jira_password'
jira_issue_transition_id '56-1'
end
after(:build) do |service, evaluator|
if evaluator.create_data
create(:jira_tracker_data, service: service,
url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id,
username: evaluator.username, password: evaluator.password
)
end
end
end end
factory :bugzilla_service do factory :bugzilla_service do
...@@ -80,20 +88,26 @@ FactoryBot.define do ...@@ -80,20 +88,26 @@ FactoryBot.define do
end end
trait :issue_tracker do trait :issue_tracker do
properties( transient do
project_url: 'http://issue-tracker.example.com', create_data true
issues_url: 'http://issue-tracker.example.com/issues/:id', project_url 'http://issuetracker.example.com'
new_issue_url: 'http://issue-tracker.example.com' issues_url 'http://issues.example.com/issues/:id'
) new_issue_url 'http://new-issue.example.com'
end
after(:build) do |service, evaluator|
if evaluator.create_data
create(:issue_tracker_data, service: service,
project_url: evaluator.project_url, issues_url: evaluator.issues_url, new_issue_url: evaluator.new_issue_url
)
end
end
end end
trait :jira_cloud_service do trait :jira_cloud_service do
properties( url 'https://mysite.atlassian.net'
url: 'https://mysite.atlassian.net', username 'jira_user'
username: 'jira_user', password 'my-secret-password'
password: 'my-secret-password',
project_key: 'jira-key'
)
end end
factory :hipchat_service do factory :hipchat_service do
...@@ -102,15 +116,21 @@ FactoryBot.define do ...@@ -102,15 +116,21 @@ FactoryBot.define do
token 'test_token' token 'test_token'
end end
# this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
trait :without_properties_callback do trait :without_properties_callback do
jira_tracker_data nil
issue_tracker_data nil
create_data false
after(:build) do |service| after(:build) do |service|
allow(service).to receive(:handle_properties) IssueTrackerService.skip_callback(:validation, :before, :handle_properties)
end end
after(:create) do |service| to_create { |instance| instance.save(validate: false)}
# we have to remove the stub because the behaviour of
# handle_properties method is tested after the creation after(:create) do
allow(service).to receive(:handle_properties).and_call_original IssueTrackerService.set_callback(:validation, :before, :handle_properties)
end end
end end
end end
...@@ -15,7 +15,10 @@ ...@@ -15,7 +15,10 @@
"author": { "author": {
"oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }] "oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }]
}, },
"milestone": { "type": "string" }, "milestones": {
"type": "array",
"items": { "$ref": "milestone.json" }
},
"assets": { "assets": {
"required": ["count", "links", "sources"], "required": ["count", "links", "sources"],
"properties": { "properties": {
......
...@@ -34,7 +34,7 @@ describe Banzai::Pipeline::GfmPipeline do ...@@ -34,7 +34,7 @@ describe Banzai::Pipeline::GfmPipeline do
result = described_class.call(markdown, project: project)[:output] result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first link = result.css('a').first
expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' expect(link['href']).to eq 'http://issues.example.com/issues/12'
end end
it 'parses cross-project references to regular issues' do it 'parses cross-project references to regular issues' do
...@@ -63,7 +63,7 @@ describe Banzai::Pipeline::GfmPipeline do ...@@ -63,7 +63,7 @@ describe Banzai::Pipeline::GfmPipeline do
result = described_class.call(markdown, project: project)[:output] result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first link = result.css('a').first
expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' expect(link['href']).to eq 'http://issues.example.com/issues/12'
end end
it 'allows to use long external reference syntax for Redmine' do it 'allows to use long external reference syntax for Redmine' do
...@@ -72,7 +72,7 @@ describe Banzai::Pipeline::GfmPipeline do ...@@ -72,7 +72,7 @@ describe Banzai::Pipeline::GfmPipeline do
result = described_class.call(markdown, project: project)[:output] result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first link = result.css('a').first
expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12' expect(link['href']).to eq 'http://issues.example.com/issues/12'
end end
it 'parses cross-project references to regular issues' do it 'parses cross-project references to regular issues' do
......
...@@ -22,11 +22,13 @@ describe Gitlab::DiscussionsDiff::FileCollection do ...@@ -22,11 +22,13 @@ describe Gitlab::DiscussionsDiff::FileCollection do
note_diff_file_b.id => file_b_caching_content }) note_diff_file_b.id => file_b_caching_content })
.and_call_original .and_call_original
subject.load_highlight([note_diff_file_a.id, note_diff_file_b.id]) subject.load_highlight
end end
it 'does not write cache for already cached file' do it 'does not write cache for already cached file' do
subject.load_highlight([note_diff_file_a.id]) file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash)
Gitlab::DiscussionsDiff::HighlightCache
.write_multiple({ note_diff_file_a.id => file_a_caching_content })
file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash) file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash)
...@@ -35,27 +37,42 @@ describe Gitlab::DiscussionsDiff::FileCollection do ...@@ -35,27 +37,42 @@ describe Gitlab::DiscussionsDiff::FileCollection do
.with({ note_diff_file_b.id => file_b_caching_content }) .with({ note_diff_file_b.id => file_b_caching_content })
.and_call_original .and_call_original
subject.load_highlight([note_diff_file_a.id, note_diff_file_b.id]) subject.load_highlight
end end
it 'does not err when given ID does not exist in @collection' do it 'does not write cache for resolved notes' do
expect { subject.load_highlight([999]) }.not_to raise_error diff_note_a.update_column(:resolved_at, Time.now)
file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash)
expect(Gitlab::DiscussionsDiff::HighlightCache)
.to receive(:write_multiple)
.with({ note_diff_file_b.id => file_b_caching_content })
.and_call_original
subject.load_highlight
end end
it 'loaded diff files have highlighted lines loaded' do it 'loaded diff files have highlighted lines loaded' do
subject.load_highlight([note_diff_file_a.id]) subject.load_highlight
diff_file = subject.find_by_id(note_diff_file_a.id) diff_file_a = subject.find_by_id(note_diff_file_a.id)
diff_file_b = subject.find_by_id(note_diff_file_b.id)
expect(diff_file.highlight_loaded?).to be(true) expect(diff_file_a).to be_highlight_loaded
expect(diff_file_b).to be_highlight_loaded
end end
it 'not loaded diff files does not have highlighted lines loaded' do it 'not loaded diff files does not have highlighted lines loaded' do
subject.load_highlight([note_diff_file_a.id]) diff_note_a.update_column(:resolved_at, Time.now)
subject.load_highlight
diff_file = subject.find_by_id(note_diff_file_b.id) diff_file_a = subject.find_by_id(note_diff_file_a.id)
diff_file_b = subject.find_by_id(note_diff_file_b.id)
expect(diff_file.highlight_loaded?).to be(false) expect(diff_file_a).not_to be_highlight_loaded
expect(diff_file_b).to be_highlight_loaded
end end
end end
end end
...@@ -65,8 +65,8 @@ milestone: ...@@ -65,8 +65,8 @@ milestone:
- participants - participants
- events - events
- boards - boards
- milestone_release - milestone_releases
- release - releases
snippets: snippets:
- author - author
- project - project
...@@ -77,8 +77,8 @@ releases: ...@@ -77,8 +77,8 @@ releases:
- author - author
- project - project
- links - links
- milestone_release - milestone_releases
- milestone - milestones
links: links:
- release - release
project_members: project_members:
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::UsageData do describe Gitlab::UsageData do
let(:projects) { create_list(:project, 3) } let(:projects) { create_list(:project, 4) }
let!(:board) { create(:board, project: projects[0]) } let!(:board) { create(:board, project: projects[0]) }
describe '#data' do describe '#data' do
before do before do
create(:jira_service, project: projects[0]) create(:jira_service, project: projects[0])
create(:jira_service, project: projects[1]) create(:jira_service, :without_properties_callback, project: projects[1])
create(:jira_service, :jira_cloud_service, project: projects[2]) create(:jira_service, :jira_cloud_service, project: projects[2])
create(:jira_service, :without_properties_callback, project: projects[3],
properties: { url: 'https://mysite.atlassian.net' })
create(:prometheus_service, project: projects[1]) create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true) create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true)
...@@ -156,7 +158,7 @@ describe Gitlab::UsageData do ...@@ -156,7 +158,7 @@ describe Gitlab::UsageData do
count_data = subject[:counts] count_data = subject[:counts]
expect(count_data[:boards]).to eq(1) expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(3) expect(count_data[:projects]).to eq(4)
expect(count_data.keys).to include(*expected_keys) expect(count_data.keys).to include(*expected_keys)
expect(expected_keys - count_data.keys).to be_empty expect(expected_keys - count_data.keys).to be_empty
end end
...@@ -164,14 +166,14 @@ describe Gitlab::UsageData do ...@@ -164,14 +166,14 @@ describe Gitlab::UsageData do
it 'gathers projects data correctly' do it 'gathers projects data correctly' do
count_data = subject[:counts] count_data = subject[:counts]
expect(count_data[:projects]).to eq(3) expect(count_data[:projects]).to eq(4)
expect(count_data[:projects_prometheus_active]).to eq(1) expect(count_data[:projects_prometheus_active]).to eq(1)
expect(count_data[:projects_jira_active]).to eq(3) expect(count_data[:projects_jira_active]).to eq(4)
expect(count_data[:projects_jira_server_active]).to eq(2) expect(count_data[:projects_jira_server_active]).to eq(2)
expect(count_data[:projects_jira_cloud_active]).to eq(1) expect(count_data[:projects_jira_cloud_active]).to eq(2)
expect(count_data[:projects_slack_notifications_active]).to eq(2) expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1) expect(count_data[:projects_slack_slash_active]).to eq(1)
expect(count_data[:projects_with_repositories_enabled]).to eq(2) expect(count_data[:projects_with_repositories_enabled]).to eq(3)
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
expect(count_data[:clusters_enabled]).to eq(7) expect(count_data[:clusters_enabled]).to eq(7)
......
...@@ -650,9 +650,35 @@ describe MergeRequest do ...@@ -650,9 +650,35 @@ describe MergeRequest do
end end
end end
describe '#preload_discussions_diff_highlight' do describe '#discussions_diffs' do
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
shared_examples 'discussions diffs collection' do
it 'initializes Gitlab::DiscussionsDiff::FileCollection with correct data' do
note_diff_file = diff_note.note_diff_file
expect(Gitlab::DiscussionsDiff::FileCollection)
.to receive(:new)
.with([note_diff_file])
.and_call_original
result = merge_request.discussions_diffs
expect(result).to be_a(Gitlab::DiscussionsDiff::FileCollection)
end
it 'eager loads relations' do
result = merge_request.discussions_diffs
recorder = ActiveRecord::QueryRecorder.new do
result.first.diff_note
result.first.diff_note.project
end
expect(recorder.count).to be_zero
end
end
context 'with commit diff note' do context 'with commit diff note' do
let(:other_merge_request) { create(:merge_request) } let(:other_merge_request) { create(:merge_request) }
...@@ -664,40 +690,15 @@ describe MergeRequest do ...@@ -664,40 +690,15 @@ describe MergeRequest do
create(:diff_note_on_commit, project: other_merge_request.project) create(:diff_note_on_commit, project: other_merge_request.project)
end end
it 'preloads diff highlighting' do it_behaves_like 'discussions diffs collection'
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
note_diff_file = diff_note.note_diff_file
expect(collection)
.to receive(:load_highlight)
.with([note_diff_file.id]).and_call_original
end
merge_request.preload_discussions_diff_highlight
end
end end
context 'with merge request diff note' do context 'with merge request diff note' do
let!(:unresolved_diff_note) do let!(:diff_note) do
create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
end end
let!(:resolved_diff_note) do it_behaves_like 'discussions diffs collection'
create(:diff_note_on_merge_request, :resolved, project: merge_request.project, noteable: merge_request)
end
it 'preloads diff highlighting' do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
note_diff_file = unresolved_diff_note.note_diff_file
expect(collection)
.to receive(:load_highlight)
.with([note_diff_file.id])
.and_call_original
end
merge_request.preload_discussions_diff_highlight
end
end end
end end
......
...@@ -14,23 +14,29 @@ describe MilestoneRelease do ...@@ -14,23 +14,29 @@ describe MilestoneRelease do
it { is_expected.to belong_to(:release) } it { is_expected.to belong_to(:release) }
end end
context 'when trying to create the same record in milestone_releases twice' do
it 'is not committing on the second time' do
create(:milestone_release, milestone: milestone, release: release)
expect do
subject.save!
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end
describe 'validations' do describe 'validations' do
it { is_expected.to validate_uniqueness_of(:milestone_id).scoped_to(:release_id) } subject(:milestone_release) { build(:milestone_release, milestone: milestone, release: release) }
context 'when milestone and release do not have the same project' do context 'when milestone and release do not have the same project' do
it 'is not valid' do it 'is not valid' do
other_project = create(:project) milestone_release.release = build(:release, project: create(:project))
release = build(:release, project: other_project)
milestone_release = described_class.new(milestone: milestone, release: release)
expect(milestone_release).not_to be_valid expect(milestone_release).not_to be_valid
end end
end end
context 'when milestone and release have the same project' do context 'when milestone and release have the same project' do
it 'is valid' do it { is_expected.to be_valid }
milestone_release = described_class.new(milestone: milestone, release: release)
expect(milestone_release).to be_valid
end
end end
end end
end end
...@@ -55,20 +55,20 @@ describe Milestone do ...@@ -55,20 +55,20 @@ describe Milestone do
end end
end end
describe 'milestone_release' do describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) } let(:milestone) { build(:milestone, project: project) }
context 'when it is tied to a release for another project' do context 'when it is tied to a release for another project' do
it 'creates a validation error' do it 'creates a validation error' do
other_project = create(:project) other_project = create(:project)
milestone.release = build(:release, project: other_project) milestone.releases << build(:release, project: other_project)
expect(milestone).not_to be_valid expect(milestone).not_to be_valid
end end
end end
context 'when it is tied to a release for the same project' do context 'when it is tied to a release for the same project' do
it 'is valid' do it 'is valid' do
milestone.release = build(:release, project: project) milestone.releases << build(:release, project: project)
expect(milestone).to be_valid expect(milestone).to be_valid
end end
end end
...@@ -78,7 +78,8 @@ describe Milestone do ...@@ -78,7 +78,8 @@ describe Milestone do
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:issues) } it { is_expected.to have_many(:issues) }
it { is_expected.to have_one(:release) } it { is_expected.to have_many(:releases) }
it { is_expected.to have_many(:milestone_releases) }
end end
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
......
...@@ -48,7 +48,7 @@ describe BugzillaService do ...@@ -48,7 +48,7 @@ describe BugzillaService do
create(:bugzilla_service, :without_properties_callback, properties: properties) create(:bugzilla_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
...@@ -56,7 +56,7 @@ describe BugzillaService do ...@@ -56,7 +56,7 @@ describe BugzillaService do
create(:bugzilla_service, title: title, description: description, properties: access_params) create(:bugzilla_service, title: title, description: description, properties: access_params)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
...@@ -65,7 +65,7 @@ describe BugzillaService do ...@@ -65,7 +65,7 @@ describe BugzillaService do
create(:bugzilla_service, :without_properties_callback, title: title, description: description, properties: properties) create(:bugzilla_service, :without_properties_callback, title: title, description: description, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
......
...@@ -62,7 +62,7 @@ describe CustomIssueTrackerService do ...@@ -62,7 +62,7 @@ describe CustomIssueTrackerService do
create(:custom_issue_tracker_service, :without_properties_callback, properties: properties) create(:custom_issue_tracker_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
...@@ -70,7 +70,7 @@ describe CustomIssueTrackerService do ...@@ -70,7 +70,7 @@ describe CustomIssueTrackerService do
create(:custom_issue_tracker_service, title: title, description: description, properties: access_params) create(:custom_issue_tracker_service, title: title, description: description, properties: access_params)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
...@@ -79,7 +79,7 @@ describe CustomIssueTrackerService do ...@@ -79,7 +79,7 @@ describe CustomIssueTrackerService do
create(:custom_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties) create(:custom_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
......
# frozen_string_literal: true
require 'spec_helper'
describe DataFields do
let(:url) { 'http://url.com' }
let(:username) { 'username_one' }
let(:properties) do
{ url: url, username: username }
end
shared_examples 'data fields' do
describe '#arg' do
it 'returns an argument correctly' do
expect(service.url).to eq(url)
end
end
describe '{arg}_changed?' do
it 'returns false when the property has not been assigned a new value' do
service.username = 'new_username'
service.validate
expect(service.url_changed?).to be_falsy
end
it 'returns true when the property has been assigned a different value' do
service.url = "http://example.com"
service.validate
expect(service.url_changed?).to be_truthy
end
it 'returns true when the property has been assigned a different value twice' do
service.url = "http://example.com"
service.url = "http://example.com"
service.validate
expect(service.url_changed?).to be_truthy
end
it 'returns false when the property has been re-assigned the same value' do
service.url = 'http://url.com'
service.validate
expect(service.url_changed?).to be_falsy
end
end
describe '{arg}_touched?' do
it 'returns false when the property has not been assigned a new value' do
service.username = 'new_username'
service.validate
expect(service.url_changed?).to be_falsy
end
it 'returns true when the property has been assigned a different value' do
service.url = "http://example.com"
service.validate
expect(service.url_changed?).to be_truthy
end
it 'returns true when the property has been assigned a different value twice' do
service.url = "http://example.com"
service.url = "http://example.com"
service.validate
expect(service.url_changed?).to be_truthy
end
it 'returns true when the property has been re-assigned the same value' do
service.url = 'http://url.com'
expect(service.url_touched?).to be_truthy
end
it 'returns false when the property has been re-assigned the same value' do
service.url = 'http://url.com'
service.validate
expect(service.url_changed?).to be_falsy
end
end
end
context 'when data are stored in data_fields' do
let(:service) do
create(:jira_service, url: url, username: username)
end
it_behaves_like 'data fields'
describe '{arg}_was?' do
it 'returns nil' do
service.url = 'http://example.com'
service.validate
expect(service.url_was).to be_nil
end
end
end
context 'when data are stored in properties' do
let(:service) { create(:jira_service, :without_properties_callback, properties: properties) }
it_behaves_like 'data fields'
describe '{arg}_was?' do
it 'returns nil when the property has not been assigned a new value' do
service.username = 'new_username'
service.validate
expect(service.url_was).to be_nil
end
it 'returns initial value when the property has been assigned a different value' do
service.url = 'http://example.com'
service.validate
expect(service.url_was).to eq('http://url.com')
end
it 'returns initial value when the property has been re-assigned the same value' do
service.url = 'http://url.com'
service.validate
expect(service.url_was).to eq('http://url.com')
end
end
end
context 'when data are stored in both properties and data_fields' do
let(:service) do
create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
create(:jira_tracker_data, properties.merge(service: service))
end
end
it_behaves_like 'data fields'
describe '{arg}_was?' do
it 'returns nil' do
service.url = 'http://example.com'
service.validate
expect(service.url_was).to be_nil
end
end
end
end
...@@ -65,7 +65,7 @@ describe GitlabIssueTrackerService do ...@@ -65,7 +65,7 @@ describe GitlabIssueTrackerService do
create(:gitlab_issue_tracker_service, :without_properties_callback, properties: properties) create(:gitlab_issue_tracker_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
...@@ -73,7 +73,7 @@ describe GitlabIssueTrackerService do ...@@ -73,7 +73,7 @@ describe GitlabIssueTrackerService do
create(:gitlab_issue_tracker_service, title: title, description: description, properties: access_params) create(:gitlab_issue_tracker_service, title: title, description: description, properties: access_params)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
...@@ -82,7 +82,7 @@ describe GitlabIssueTrackerService do ...@@ -82,7 +82,7 @@ describe GitlabIssueTrackerService do
create(:gitlab_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties) create(:gitlab_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
......
...@@ -8,28 +8,4 @@ describe IssueTrackerData do ...@@ -8,28 +8,4 @@ describe IssueTrackerData do
describe 'Associations' do describe 'Associations' do
it { is_expected.to belong_to :service } it { is_expected.to belong_to :service }
end end
describe 'Validations' do
subject { described_class.new(service: service) }
context 'url validations' do
context 'when service is inactive' do
it { is_expected.not_to validate_presence_of(:project_url) }
it { is_expected.not_to validate_presence_of(:issues_url) }
end
context 'when service is active' do
before do
service.update(active: true)
end
it_behaves_like 'issue tracker service URL attribute', :project_url
it_behaves_like 'issue tracker service URL attribute', :issues_url
it_behaves_like 'issue tracker service URL attribute', :new_issue_url
it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) }
end
end
end
end end
...@@ -7,7 +7,7 @@ describe IssueTrackerService do ...@@ -7,7 +7,7 @@ describe IssueTrackerService do
let(:project) { create :project } let(:project) { create :project }
describe 'only one issue tracker per project' do describe 'only one issue tracker per project' do
let(:service) { RedmineService.new(project: project, active: true) } let(:service) { RedmineService.new(project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
before do before do
create(:custom_issue_tracker_service, project: project) create(:custom_issue_tracker_service, project: project)
......
...@@ -8,35 +8,4 @@ describe JiraTrackerData do ...@@ -8,35 +8,4 @@ describe JiraTrackerData do
describe 'Associations' do describe 'Associations' do
it { is_expected.to belong_to(:service) } it { is_expected.to belong_to(:service) }
end end
describe 'Validations' do
subject { described_class.new(service: service) }
context 'jira_issue_transition_id' do
it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) }
it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) }
it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) }
it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) }
end
context 'url validations' do
context 'when service is inactive' do
it { is_expected.not_to validate_presence_of(:url) }
it { is_expected.not_to validate_presence_of(:username) }
it { is_expected.not_to validate_presence_of(:password) }
end
context 'when service is active' do
before do
service.update(active: true)
end
it_behaves_like 'issue tracker service URL attribute', :url
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_presence_of(:username) }
it { is_expected.to validate_presence_of(:password) }
end
end
end
end end
...@@ -9,6 +9,15 @@ describe RedmineService do ...@@ -9,6 +9,15 @@ describe RedmineService do
end end
describe 'Validations' do describe 'Validations' do
# if redmine is set in setting the urls are set to defaults
# therefore the validation passes as the values are not nil
before do
settings = {
'redmine' => {}
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
end
context 'when service is active' do context 'when service is active' do
before do before do
subject.active = true subject.active = true
...@@ -17,6 +26,7 @@ describe RedmineService do ...@@ -17,6 +26,7 @@ describe RedmineService do
it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) } it { is_expected.to validate_presence_of(:issues_url) }
it { is_expected.to validate_presence_of(:new_issue_url) } it { is_expected.to validate_presence_of(:new_issue_url) }
it_behaves_like 'issue tracker service URL attribute', :project_url it_behaves_like 'issue tracker service URL attribute', :project_url
it_behaves_like 'issue tracker service URL attribute', :issues_url it_behaves_like 'issue tracker service URL attribute', :issues_url
it_behaves_like 'issue tracker service URL attribute', :new_issue_url it_behaves_like 'issue tracker service URL attribute', :new_issue_url
...@@ -54,7 +64,7 @@ describe RedmineService do ...@@ -54,7 +64,7 @@ describe RedmineService do
create(:redmine_service, :without_properties_callback, properties: properties) create(:redmine_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
...@@ -62,7 +72,7 @@ describe RedmineService do ...@@ -62,7 +72,7 @@ describe RedmineService do
create(:redmine_service, title: title, description: description, properties: access_params) create(:redmine_service, title: title, description: description, properties: access_params)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
...@@ -71,7 +81,7 @@ describe RedmineService do ...@@ -71,7 +81,7 @@ describe RedmineService do
create(:redmine_service, :without_properties_callback, title: title, description: description, properties: properties) create(:redmine_service, :without_properties_callback, title: title, description: description, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
......
...@@ -16,6 +16,7 @@ describe YoutrackService do ...@@ -16,6 +16,7 @@ describe YoutrackService do
it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) } it { is_expected.to validate_presence_of(:issues_url) }
it_behaves_like 'issue tracker service URL attribute', :project_url it_behaves_like 'issue tracker service URL attribute', :project_url
it_behaves_like 'issue tracker service URL attribute', :issues_url it_behaves_like 'issue tracker service URL attribute', :issues_url
end end
...@@ -51,7 +52,7 @@ describe YoutrackService do ...@@ -51,7 +52,7 @@ describe YoutrackService do
create(:youtrack_service, :without_properties_callback, properties: properties) create(:youtrack_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
...@@ -59,7 +60,7 @@ describe YoutrackService do ...@@ -59,7 +60,7 @@ describe YoutrackService do
create(:youtrack_service, title: title, description: description, properties: access_params) create(:youtrack_service, title: title, description: description, properties: access_params)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
...@@ -68,7 +69,7 @@ describe YoutrackService do ...@@ -68,7 +69,7 @@ describe YoutrackService do
create(:youtrack_service, :without_properties_callback, title: title, description: description, properties: properties) create(:youtrack_service, :without_properties_callback, title: title, description: description, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
......
...@@ -13,7 +13,8 @@ RSpec.describe Release do ...@@ -13,7 +13,8 @@ RSpec.describe Release do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to have_many(:links).class_name('Releases::Link') } it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_one(:milestone) } it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) }
end end
describe 'validation' do describe 'validation' do
...@@ -38,15 +39,15 @@ RSpec.describe Release do ...@@ -38,15 +39,15 @@ RSpec.describe Release do
context 'when a release is tied to a milestone for another project' do context 'when a release is tied to a milestone for another project' do
it 'creates a validation error' do it 'creates a validation error' do
release.milestone = build(:milestone, project: create(:project)) milestone = build(:milestone, project: create(:project))
expect(release).not_to be_valid expect { release.milestones << milestone }.to raise_error
end end
end end
context 'when a release is tied to a milestone linked to the same project' do context 'when a release is tied to a milestone linked to the same project' do
it 'is valid' do it 'successfully links this release to this milestone' do
release.milestone = build(:milestone, project: project) milestone = build(:milestone, project: project)
expect(release).to be_valid expect { release.milestones << milestone }.to change { MilestoneRelease.count }.by(1)
end end
end end
end end
......
...@@ -257,8 +257,8 @@ describe Service do ...@@ -257,8 +257,8 @@ describe Service do
expect(service.title).to eq('random title') expect(service.title).to eq('random title')
end end
it 'creates the properties' do it 'sets data correctly' do
expect(service.properties).to eq({ "project_url" => "http://gitlab.example.com" }) expect(service.data_fields.project_url).to eq('http://gitlab.example.com')
end end
end end
......
...@@ -100,9 +100,15 @@ describe API::Services do ...@@ -100,9 +100,15 @@ describe API::Services do
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
end end
it "returns empty hash if properties are empty" do it "returns empty hash if properties and data fields are empty" do
# deprecated services are not valid for update # deprecated services are not valid for update
initialized_service.update_attribute(:properties, {}) initialized_service.update_attribute(:properties, {})
if initialized_service.data_fields_present?
initialized_service.data_fields.destroy
initialized_service.reload
end
get api("/projects/#{project.id}/services/#{dashed_service}", user) get api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
......
# frozen_string_literal: true
require 'spec_helper'
describe 'merge requests discussions' do
# Further tests can be found at merge_requests_controller_spec.rb
describe 'GET /:namespace/:project/merge_requests/:iid/discussions' do
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
before do
project.add_developer(user)
login_as(user)
end
def send_request
get discussions_namespace_project_merge_request_path(namespace_id: project.namespace, project_id: project, id: merge_request.iid)
end
it 'returns 200' do
send_request
expect(response.status).to eq(200)
end
# https://docs.gitlab.com/ee/development/query_recorder.html#use-request-specs-instead-of-controller-specs
it 'avoids N+1 DB queries', :request_store do
control = ActiveRecord::QueryRecorder.new { send_request }
create(:diff_note_on_merge_request, noteable: merge_request,
project: merge_request.project)
expect do
send_request
end.not_to exceed_query_limit(control)
end
it 'limits Gitaly queries', :request_store do
Gitlab::GitalyClient.allow_n_plus_1_calls do
create_list(:diff_note_on_merge_request, 7, noteable: merge_request,
project: merge_request.project)
end
# The creations above write into the Gitaly counts
Gitlab::GitalyClient.reset_counts
expect { send_request }
.to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4)
end
end
end
...@@ -72,7 +72,7 @@ describe Milestones::DestroyService do ...@@ -72,7 +72,7 @@ describe Milestones::DestroyService do
:release, :release,
tag: 'v1.0', tag: 'v1.0',
project: project, project: project,
milestone: milestone milestones: [milestone]
) )
expect { service.execute(milestone) }.not_to change { Release.count } expect { service.execute(milestone) }.not_to change { Release.count }
......
...@@ -75,10 +75,12 @@ describe Releases::CreateService do ...@@ -75,10 +75,12 @@ describe Releases::CreateService do
context 'when a passed-in milestone does not exist for this project' do context 'when a passed-in milestone does not exist for this project' do
it 'raises an error saying the milestone is inexistent' do it 'raises an error saying the milestone is inexistent' do
service = described_class.new(project, user, params.merge!({ milestone: 'v111.0' })) inexistent_milestone_tag = 'v111.0'
service = described_class.new(project, user, params.merge!({ milestones: [inexistent_milestone_tag] }))
result = service.execute result = service.execute
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Milestone does not exist') expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end end
end end
end end
...@@ -93,10 +95,10 @@ describe Releases::CreateService do ...@@ -93,10 +95,10 @@ describe Releases::CreateService do
context 'when existing milestone is passed in' do context 'when existing milestone is passed in' do
let(:title) { 'v1.0' } let(:title) { 'v1.0' }
let(:milestone) { create(:milestone, :active, project: project, title: title) } let(:milestone) { create(:milestone, :active, project: project, title: title) }
let(:params_with_milestone) { params.merge!({ milestone: title }) } let(:params_with_milestone) { params.merge!({ milestones: [title] }) }
let(:service) { described_class.new(milestone.project, user, params_with_milestone) }
it 'creates a release and ties this milestone to it' do it 'creates a release and ties this milestone to it' do
service = described_class.new(milestone.project, user, params_with_milestone)
result = service.execute result = service.execute
expect(project.releases.count).to eq(1) expect(project.releases.count).to eq(1)
...@@ -104,29 +106,66 @@ describe Releases::CreateService do ...@@ -104,29 +106,66 @@ describe Releases::CreateService do
release = project.releases.last release = project.releases.last
expect(release.milestone).to eq(milestone) expect(release.milestones).to match_array([milestone])
end end
context 'when another release was previously created with that same milestone linked' do context 'when another release was previously created with that same milestone linked' do
it 'also creates another release tied to that same milestone' do it 'also creates another release tied to that same milestone' do
other_release = create(:release, milestone: milestone, project: project, tag: 'v1.0') other_release = create(:release, milestones: [milestone], project: project, tag: 'v1.0')
service = described_class.new(milestone.project, user, params_with_milestone)
service.execute service.execute
release = project.releases.last release = project.releases.last
expect(release.milestone).to eq(milestone) expect(release.milestones).to match_array([milestone])
expect(other_release.milestone).to eq(milestone) expect(other_release.milestones).to match_array([milestone])
expect(release.id).not_to eq(other_release.id) expect(release.id).not_to eq(other_release.id)
end end
end end
end end
context 'when multiple existing milestone titles are passed in' do
let(:title_1) { 'v1.0' }
let(:title_2) { 'v1.0-rc' }
let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }
let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
it 'creates a release and ties it to these milestones' do
described_class.new(project, user, params_with_milestones).execute
release = project.releases.last
expect(release.milestones.map(&:title)).to include(title_1, title_2)
end
end
context 'when multiple miletone titles are passed in but one of them does not exist' do
let(:title) { 'v1.0' }
let(:inexistent_title) { 'v111.0' }
let!(:milestone) { create(:milestone, :active, project: project, title: title) }
let!(:params_with_milestones) { params.merge!({ milestones: [title, inexistent_title] }) }
let(:service) { described_class.new(milestone.project, user, params_with_milestones) }
it 'raises an error' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_title}")
end
it 'does not create any release' do
expect do
service.execute
end.not_to change(Release, :count)
end
end
context 'when no milestone is passed in' do context 'when no milestone is passed in' do
it 'creates a release without a milestone tied to it' do it 'creates a release without a milestone tied to it' do
expect(params.key? :milestone).to be_falsey expect(params.key? :milestones).to be_falsey
service.execute service.execute
release = project.releases.last release = project.releases.last
expect(release.milestone).to be_nil
expect(release.milestones).to be_empty
end end
it 'does not create any new MilestoneRelease object' do it 'does not create any new MilestoneRelease object' do
...@@ -136,10 +175,11 @@ describe Releases::CreateService do ...@@ -136,10 +175,11 @@ describe Releases::CreateService do
context 'when an empty value is passed as a milestone' do context 'when an empty value is passed as a milestone' do
it 'creates a release without a milestone tied to it' do it 'creates a release without a milestone tied to it' do
service = described_class.new(project, user, params.merge!({ milestone: '' })) service = described_class.new(project, user, params.merge!({ milestones: [] }))
service.execute service.execute
release = project.releases.last release = project.releases.last
expect(release.milestone).to be_nil
expect(release.milestones).to be_empty
end end
end end
end end
......
...@@ -60,7 +60,7 @@ describe Releases::DestroyService do ...@@ -60,7 +60,7 @@ describe Releases::DestroyService do
context 'when a milestone is tied to the release' do context 'when a milestone is tied to the release' do
let!(:milestone) { create(:milestone, :active, project: project, title: 'v1.0') } let!(:milestone) { create(:milestone, :active, project: project, title: 'v1.0') }
let!(:release) { create(:release, milestone: milestone, project: project, tag: tag) } let!(:release) { create(:release, milestones: [milestone], project: project, tag: tag) }
it 'destroys the release but leave the milestone intact' do it 'destroys the release but leave the milestone intact' do
expect { subject }.not_to change { Milestone.count } expect { subject }.not_to change { Milestone.count }
......
...@@ -50,39 +50,60 @@ describe Releases::UpdateService do ...@@ -50,39 +50,60 @@ describe Releases::UpdateService do
end end
context 'when a milestone is passed in' do context 'when a milestone is passed in' do
let(:old_title) { 'v1.0' }
let(:new_title) { 'v2.0' } let(:new_title) { 'v2.0' }
let(:milestone) { create(:milestone, project: project, title: old_title) } let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:new_milestone) { create(:milestone, project: project, title: new_title) } let(:new_milestone) { create(:milestone, project: project, title: new_title) }
let(:params_with_milestone) { params.merge!({ milestone: new_title }) } let(:params_with_milestone) { params.merge!({ milestones: [new_title] }) }
let(:service) { described_class.new(new_milestone.project, user, params_with_milestone) }
before do before do
release.milestone = milestone release.milestones << milestone
release.save!
described_class.new(new_milestone.project, user, params_with_milestone).execute service.execute
release.reload release.reload
end end
it 'updates the related milestone accordingly' do it 'updates the related milestone accordingly' do
expect(release.milestone.title).to eq(new_title) expect(release.milestones.first.title).to eq(new_title)
end end
end end
context "when an 'empty' milestone is passed in" do context "when an 'empty' milestone is passed in" do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') } let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:params_with_empty_milestone) { params.merge!({ milestone: '' }) } let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
before do before do
release.milestone = milestone release.milestones << milestone
release.save!
described_class.new(milestone.project, user, params_with_empty_milestone).execute service.params = params_with_empty_milestone
service.execute
release.reload release.reload
end end
it 'removes the old milestone and does not associate any new milestone' do it 'removes the old milestone and does not associate any new milestone' do
expect(release.milestone).to be_nil expect(release.milestones).not_to be_present
end
end
context "when multiple new milestones are passed in" do
let(:new_title_1) { 'v2.0' }
let(:new_title_2) { 'v2.0-rc' }
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
let(:service) { described_class.new(project, user, params_with_milestones) }
before do
create(:milestone, project: project, title: new_title_1)
create(:milestone, project: project, title: new_title_2)
release.milestones << milestone
service.execute
release.reload
end
it 'removes the old milestone and update the release with the new ones' do
milestone_titles = release.milestones.map(&:title)
expect(milestone_titles).to match_array([new_title_1, new_title_2])
end end
end end
end end
......
...@@ -5,16 +5,16 @@ module JiraServiceHelper ...@@ -5,16 +5,16 @@ module JiraServiceHelper
JIRA_API = JIRA_URL + "/rest/api/2" JIRA_API = JIRA_URL + "/rest/api/2"
def jira_service_settings def jira_service_settings
properties = { title = "Jira tracker"
title: "Jira tracker", url = JIRA_URL
url: JIRA_URL, username = 'jira-user'
username: 'jira-user', password = 'my-secret-password'
password: 'my-secret-password', jira_issue_transition_id = '1'
project_key: "JIRA",
jira_issue_transition_id: '1'
}
jira_tracker.update(properties: properties, active: true) jira_tracker.update(
title: title, url: url, username: username, password: password,
jira_issue_transition_id: jira_issue_transition_id, active: true
)
end end
def jira_issue_comments def jira_issue_comments
......
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