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?
if enabled_in_gitlab_config self.title ||= issues_tracker['title']
if block_given?
yield # we don't want to override if we have set something
else return if project_url || issues_url || new_issue_url
self.properties = {
title: issues_tracker['title'], data_fields.project_url = issues_tracker['project_url']
project_url: issues_tracker['project_url'], data_fields.issues_url = issues_tracker['issues_url']
issues_url: issues_tracker['issues_url'], data_fields.new_issue_url = issues_tracker['new_issue_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 = {
url: issues_tracker['url'],
api_url: issues_tracker['api_url']
}
end end
def data_fields
jira_tracker_data || self.build_jira_tracker_data
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
......
...@@ -57,7 +57,8 @@ Example response: ...@@ -57,7 +57,8 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:55:38.000Z" "committed_date":"2019-01-03T01:55:38.000Z"
}, },
"milestone":{ "milestones": [
{
"id":51, "id":51,
"iid":1, "iid":1,
"project_id":24, "project_id":24,
...@@ -68,8 +69,22 @@ Example response: ...@@ -68,8 +69,22 @@ Example response:
"updated_at":"2019-07-12T19:45:44.256Z", "updated_at":"2019-07-12T19:45:44.256Z",
"due_date":"2019-08-16T11:00:00.256Z", "due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z", "start_date":"2019-07-30T12:00:00.256Z",
"web_url":"http://localhost:3000/root/awesome-app/-/milestones/1" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/1"
}, },
{
"id":52,
"iid":2,
"project_id":24,
"title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-16T14:00:12.256Z",
"updated_at":"2019-07-16T14:00:12.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
}
],
"assets":{ "assets":{
"count":6, "count":6,
"sources":[ "sources":[
...@@ -218,7 +233,8 @@ Example response: ...@@ -218,7 +233,8 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z" "committed_date":"2019-01-03T01:53:28.000Z"
}, },
"milestone":{ "milestones": [
{
"id":51, "id":51,
"iid":1, "iid":1,
"project_id":24, "project_id":24,
...@@ -229,8 +245,22 @@ Example response: ...@@ -229,8 +245,22 @@ Example response:
"updated_at":"2019-07-12T19:45:44.256Z", "updated_at":"2019-07-12T19:45:44.256Z",
"due_date":"2019-08-16T11:00:00.256Z", "due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z", "start_date":"2019-07-30T12:00:00.256Z",
"web_url":"http://localhost:3000/root/awesome-app/-/milestones/1" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/1"
}, },
{
"id":52,
"iid":2,
"project_id":24,
"title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-16T14:00:12.256Z",
"updated_at":"2019-07-16T14:00:12.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
}
],
"assets":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
...@@ -273,7 +303,7 @@ POST /projects/:id/releases ...@@ -273,7 +303,7 @@ POST /projects/:id/releases
| `tag_name` | string | yes | The tag where the release will be created from. | | `tag_name` | string | yes | The tag where the release will be created from. |
| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). | | `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). |
| `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. | | `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. |
| `milestone` | string | no | The title of the milestone the release is associated with. | | `milestones` | array of string | no | The title of each milestone the release is associated with. |
| `assets:links` | array of hash | no | An array of assets links. | | `assets:links` | array of hash | no | An array of assets links. |
| `assets:links:name`| string | required by: `assets:links` | The name of the link. | | `assets:links:name`| string | required by: `assets:links` | The name of the link. |
| `assets:links:url` | string | required by: `assets:links` | The url of the link. | | `assets:links:url` | string | required by: `assets:links` | The url of the link. |
...@@ -283,7 +313,7 @@ Example request: ...@@ -283,7 +313,7 @@ Example request:
```sh ```sh
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \
--data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestone": "v1.0-rc", "assets": { "links": [{ "name": "hoge", "url": "https://google.com" }] } }' \ --data '{ "name": "New release", "tag_name": "v0.3", "description": "Super nice release", "milestones": ["v1.0", "v1.0-rc"], "assets": { "links": [{ "name": "hoge", "url": "https://google.com" }] } }' \
--request POST https://gitlab.example.com/api/v4/projects/24/releases --request POST https://gitlab.example.com/api/v4/projects/24/releases
``` ```
...@@ -321,19 +351,34 @@ Example response: ...@@ -321,19 +351,34 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:55:38.000Z" "committed_date":"2019-01-03T01:55:38.000Z"
}, },
"milestone":{ "milestones": [
{
"id":51, "id":51,
"iid":1, "iid":1,
"project_id":24, "project_id":24,
"title":"v1.0-rc", "title":"v1.0-rc",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.", "description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"active", "state":"closed",
"created_at":"2019-07-12T19:45:44.256Z", "created_at":"2019-07-12T19:45:44.256Z",
"updated_at":"2019-07-12T19:45:44.256Z", "updated_at":"2019-07-12T19:45:44.256Z",
"due_date":"2019-08-16T11:00:00.256Z", "due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z", "start_date":"2019-07-30T12:00:00.256Z",
"web_url":"http://localhost:3000/root/awesome-app/-/milestones/1" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/1"
}, },
{
"id":52,
"iid":2,
"project_id":24,
"title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-16T14:00:12.256Z",
"updated_at":"2019-07-16T14:00:12.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
}
],
"assets":{ "assets":{
"count":5, "count":5,
"sources":[ "sources":[
...@@ -375,18 +420,18 @@ PUT /projects/:id/releases/:tag_name ...@@ -375,18 +420,18 @@ PUT /projects/:id/releases/:tag_name
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------- | | ------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag where the release will be created from. | | `tag_name` | string | yes | The tag where the release will be created from. |
| `name` | string | no | The release name. | | `name` | string | no | The release name. |
| `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). | | `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). |
| `milestone` | string | no | The title of the milestone to associate with the release (`""` to remove the milestone from the release). | | `milestones` | array of string | no | The title of each milestone to associate with the release (`[]` to remove all milestones from the release). |
| `released_at` | datetime | no | The date when the release will be/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | | `released_at` | datetime | no | The date when the release will be/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
Example request: Example request:
```sh ```sh
curl --header 'Content-Type: application/json' --request PUT --data '{"name": "new name", "milestone": "v1.0"}' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1" curl --header 'Content-Type: application/json' --request PUT --data '{"name": "new name", "milestones": ["v1.2"]}' --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1"
``` ```
Example response: Example response:
...@@ -423,9 +468,10 @@ Example response: ...@@ -423,9 +468,10 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z" "committed_date":"2019-01-03T01:53:28.000Z"
}, },
"milestone":{ "milestones": [
{
"id":53, "id":53,
"iid":2, "iid":3,
"project_id":24, "project_id":24,
"title":"v1.0", "title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.", "description":"Voluptate fugiat possimus quis quod aliquam expedita.",
...@@ -434,8 +480,9 @@ Example response: ...@@ -434,8 +480,9 @@ Example response:
"updated_at":"2019-09-01T13:00:00.256Z", "updated_at":"2019-09-01T13:00:00.256Z",
"due_date":"2019-09-20T13:00:00.256Z", "due_date":"2019-09-20T13:00:00.256Z",
"start_date":"2019-09-05T12:00:00.256Z", "start_date":"2019-09-05T12:00:00.256Z",
"web_url":"http://localhost:3000/root/awesome-app/-/milestones/3" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/3"
}, }
],
"assets":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
......
...@@ -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,9 +1044,14 @@ module API ...@@ -1044,9 +1044,14 @@ module API
expose :job_events expose :job_events
# Expose serialized properties # Expose serialized properties
expose :properties do |service, options| expose :properties do |service, options|
# 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) service.properties.slice(*service.api_field_names)
end end
end end
end
class ProjectWithAccess < Project class ProjectWithAccess < Project
expose :permissions do expose :permissions do
...@@ -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)
...@@ -96,8 +97,7 @@ module Gitlab ...@@ -96,8 +97,7 @@ 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
...@@ -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,13 +43,25 @@ FactoryBot.define do ...@@ -47,13 +43,25 @@ 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
project project
...@@ -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,54 +650,55 @@ describe MergeRequest do ...@@ -650,54 +650,55 @@ 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) }
context 'with commit diff note' do shared_examples 'discussions diffs collection' do
let(:other_merge_request) { create(:merge_request) } it 'initializes Gitlab::DiscussionsDiff::FileCollection with correct data' do
note_diff_file = diff_note.note_diff_file
let!(:diff_note) do expect(Gitlab::DiscussionsDiff::FileCollection)
create(:diff_note_on_commit, project: merge_request.project) .to receive(:new)
end .with([note_diff_file])
.and_call_original
let!(:other_mr_diff_note) do result = merge_request.discussions_diffs
create(:diff_note_on_commit, project: other_merge_request.project)
expect(result).to be_a(Gitlab::DiscussionsDiff::FileCollection)
end end
it 'preloads diff highlighting' do it 'eager loads relations' do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| result = merge_request.discussions_diffs
note_diff_file = diff_note.note_diff_file
expect(collection) recorder = ActiveRecord::QueryRecorder.new do
.to receive(:load_highlight) result.first.diff_note
.with([note_diff_file.id]).and_call_original result.first.diff_note.project
end end
merge_request.preload_discussions_diff_highlight expect(recorder.count).to be_zero
end end
end end
context 'with merge request diff note' do context 'with commit diff note' do
let!(:unresolved_diff_note) do let(:other_merge_request) { create(:merge_request) }
create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
end
let!(:resolved_diff_note) do let!(:diff_note) do
create(:diff_note_on_merge_request, :resolved, project: merge_request.project, noteable: merge_request) create(:diff_note_on_commit, project: merge_request.project)
end end
it 'preloads diff highlighting' do let!(:other_mr_diff_note) do
expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| create(:diff_note_on_commit, project: other_merge_request.project)
note_diff_file = unresolved_diff_note.note_diff_file end
expect(collection) it_behaves_like 'discussions diffs collection'
.to receive(:load_highlight)
.with([note_diff_file.id])
.and_call_original
end end
merge_request.preload_discussions_diff_highlight context 'with merge request diff note' do
let!(:diff_note) do
create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
end end
it_behaves_like 'discussions diffs collection'
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)
......
...@@ -6,10 +6,18 @@ describe JiraService do ...@@ -6,10 +6,18 @@ describe JiraService do
include Gitlab::Routing include Gitlab::Routing
include AssetsHelpers include AssetsHelpers
let(:title) { 'custom title' }
let(:description) { 'custom description' }
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
let(:password) { 'jira-password' }
let(:transition_id) { 'test27' }
describe '#options' do describe '#options' do
let(:service) do let(:service) do
described_class.new( described_class.create(
project: build_stubbed(:project), project: create(:project),
active: true, active: true,
username: 'username', username: 'username',
password: 'test', password: 'test',
...@@ -32,141 +40,274 @@ describe JiraService do ...@@ -32,141 +40,274 @@ describe JiraService 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_one :service_hook } it { is_expected.to have_one :service_hook }
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 end
describe 'Validations' do describe '.reference_pattern' do
context 'when service is active' do it_behaves_like 'allows project key on reference pattern'
before do
subject.active = true it 'does not allow # on the code' do
expect(described_class.reference_pattern.match('#123')).to be_nil
expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end
end
describe '#create' do
let(:params) do
{
project: create(:project),
title: 'custom title', description: 'custom description',
url: url, api_url: api_url,
username: username, password: password,
jira_issue_transition_id: transition_id
}
end
subject { described_class.create(params) }
it 'does not store data into properties' do
expect(subject.properties).to be_nil
end
it 'sets title correctly' do
service = subject
expect(service.title).to eq('custom title')
end
it 'sets service data correctly' do
service = subject
expect(service.title).to eq('custom title')
expect(service.description).to eq('custom description')
end
it 'stores data in data_fields correcty' do
service = subject
expect(service.jira_tracker_data.url).to eq(url)
expect(service.jira_tracker_data.api_url).to eq(api_url)
expect(service.jira_tracker_data.username).to eq(username)
expect(service.jira_tracker_data.password).to eq(password)
expect(service.jira_tracker_data.jira_issue_transition_id).to eq(transition_id)
end
end
# we need to make sure we are able to read both from properties and jira_tracker_data table
# TODO: change this as part of #63084
context 'overriding properties' do
let(:access_params) do
{ url: url, api_url: api_url, username: username, password: password,
jira_issue_transition_id: transition_id }
end
let(:data_params) do
{
url: url, api_url: api_url,
username: username, password: password,
jira_issue_transition_id: transition_id
}
end end
it { is_expected.to validate_presence_of(:url) } shared_examples 'handles jira fields' do
it_behaves_like 'issue tracker service URL attribute', :url let(:data_params) do
{
url: url, api_url: api_url,
username: username, password: password,
jira_issue_transition_id: transition_id
}
end end
context 'when service is inactive' do context 'reading data' do
it 'reads data correctly' do
expect(service.url).to eq(url)
expect(service.api_url).to eq(api_url)
expect(service.username).to eq(username)
expect(service.password).to eq(password)
expect(service.jira_issue_transition_id).to eq(transition_id)
end
end
context '#update' do
context 'basic update' do
let(:new_username) { 'new_username' }
let(:new_url) { 'http://jira-new.example.com' }
before do before do
subject.active = false service.update(username: new_username, url: new_url)
end end
it { is_expected.not_to validate_presence_of(:url) } it 'leaves properties field emtpy' do
it { is_expected.not_to validate_presence_of(:username) } # expect(service.reload.properties).to be_empty
it { is_expected.not_to validate_presence_of(:password) }
end end
context 'validating urls' do it 'stores updated data in jira_tracker_data table' do
let(:service) do data = service.jira_tracker_data.reload
described_class.new(
project: create(:project), expect(data.url).to eq(new_url)
active: true, expect(data.api_url).to eq(api_url)
username: 'username', expect(data.username).to eq(new_username)
password: 'test', expect(data.password).to eq(password)
jira_issue_transition_id: 24, expect(data.jira_issue_transition_id).to eq(transition_id)
url: 'http://jira.test.com' end
)
end end
it 'is valid when all fields have required values' do context 'stored password invalidation' do
expect(service).to be_valid context 'when a password was previously set' do
context 'when only web url present' do
let(:data_params) do
{
url: url, api_url: nil,
username: username, password: password,
jira_issue_transition_id: transition_id
}
end end
it 'is not valid when url is not a valid url' do it 'resets password if url changed' do
service.url = 'not valid' service
service.url = 'http://jira_edited.example.com'
service.save
expect(service).not_to be_valid expect(service.reload.url).to eq('http://jira_edited.example.com')
expect(service.password).to be_nil
end end
it 'is not valid when api url is not a valid url' do it 'does not reset password if url "changed" to the same url as before' do
service.api_url = 'not valid' service.title = 'aaaaaa'
service.url = 'http://jira.example.com'
service.save
expect(service).not_to be_valid expect(service.reload.url).to eq('http://jira.example.com')
expect(service.password).not_to be_nil
end end
it 'is not valid when username is missing' do it 'resets password if url not changed but api url added' do
service.username = nil service.api_url = 'http://jira_edited.example.com/rest/api/2'
service.save
expect(service).not_to be_valid expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2')
expect(service.password).to be_nil
end end
it 'is not valid when password is missing' do it 'does not reset password if new url is set together with password, even if it\'s the same password' do
service.password = nil service.url = 'http://jira_edited.example.com'
service.password = password
service.save
expect(service).not_to be_valid expect(service.password).to eq(password)
expect(service.url).to eq('http://jira_edited.example.com')
end end
it 'is valid when api url is a valid url' do it 'resets password if url changed, even if setter called multiple times' do
service.api_url = 'http://jira.test.com/api' service.url = 'http://jira1.example.com/rest/api/2'
service.url = 'http://jira1.example.com/rest/api/2'
service.save
expect(service).to be_valid expect(service.password).to be_nil
end end
it 'does not reset password if username changed' do
service.username = 'some_name'
service.save
expect(service.reload.password).to eq(password)
end end
it 'does not reset password if password changed' do
service.url = 'http://jira_edited.example.com'
service.password = 'new_password'
service.save
expect(service.reload.password).to eq('new_password')
end end
describe '.reference_pattern' do it 'does not reset password if the password is touched and same as before' do
it_behaves_like 'allows project key on reference pattern' service.url = 'http://jira_edited.example.com'
service.password = password
service.save
it 'does not allow # on the code' do expect(service.reload.password).to eq(password)
expect(described_class.reference_pattern.match('#123')).to be_nil
expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end end
end end
describe '#create' do context 'when both web and api url present' do
let(:params) do let(:data_params) do
{ {
project: create(:project), title: 'custom title', description: 'custom description' url: url, api_url: 'http://jira.example.com/rest/api/2',
username: username, password: password,
jira_issue_transition_id: transition_id
} }
end end
subject { described_class.create(params) } it 'resets password if api url changed' do
service.api_url = 'http://jira_edited.example.com/rest/api/2'
service.save
it 'does not store title & description into properties' do expect(service.password).to be_nil
expect(subject.properties.keys).not_to include('title', 'description')
end end
it 'sets title & description correctly' do it 'does not reset password if url changed' do
service = subject service.url = 'http://jira_edited.example.com'
service.save
expect(service.title).to eq('custom title') expect(service.password).to eq(password)
expect(service.description).to eq('custom description') end
it 'resets password if api url set to empty' do
service.update(api_url: '')
expect(service.reload.password).to be_nil
end
end end
end end
context 'overriding properties' do context 'when no password was previously set' do
let(:url) { 'http://issue_tracker.example.com' } let(:data_params) do
let(:access_params) do {
{ url: url, username: 'username', password: 'password' } url: url, username: username
}
end
it 'saves password if new url is set together with password' do
service.url = 'http://jira_edited.example.com/rest/api/2'
service.password = 'password'
service.save
expect(service.reload.password).to eq('password')
expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2')
end
end
end
end
end end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084 # this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
context 'when data are stored in properties' do context 'when data are stored in properties' do
let(:properties) { access_params.merge(title: title, description: description) } let(:properties) { data_params.merge(title: title, description: description) }
let(:service) do let!(:service) do
create(:jira_service, :without_properties_callback, properties: properties) create(:jira_service, :without_properties_callback, properties: properties)
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
it_behaves_like 'handles jira fields'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
let(:service) do let(:service) do
create(:jira_service, title: title, description: description, properties: access_params) create(:jira_service, data_params.merge(properties: {}, title: title, description: description))
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
it_behaves_like 'handles jira 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
let(:properties) { access_params.merge(title: 'wrong title', description: 'wrong description') } let(:properties) { data_params.merge(title: title, description: description) }
let(:service) do let(:service) do
create(:jira_service, :without_properties_callback, title: title, description: description, properties: properties) create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
create(:jira_tracker_data, data_params.merge(service: service))
end
end end
include_examples 'issue tracker fields' it_behaves_like 'issue tracker fields'
it_behaves_like 'handles jira fields'
end end
context 'when no title & description are set' do context 'when no title & description are set' do
...@@ -410,111 +551,6 @@ describe JiraService do ...@@ -410,111 +551,6 @@ describe JiraService do
end end
end end
describe 'Stored password invalidation' do
let(:project) { create(:project) }
context 'when a password was previously set' do
before do
@jira_service = described_class.create!(
project: project,
properties: {
url: 'http://jira.example.com/web',
username: 'mic',
password: 'password'
}
)
end
context 'when only web url present' do
it 'reset password if url changed' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
it 'reset password if url not changed but api url added' do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
end
context 'when both web and api url present' do
before do
@jira_service.api_url = 'http://jira.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
end
it 'reset password if api url changed' do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
it 'does not reset password if url changed' do
@jira_service.url = 'http://jira_edited.example.com/rweb'
@jira_service.save
expect(@jira_service.password).to eq('password')
end
it 'reset password if api url set to empty' do
@jira_service.api_url = ''
@jira_service.save
expect(@jira_service.password).to be_nil
end
end
it 'does not reset password if username changed' do
@jira_service.username = 'some_name'
@jira_service.save
expect(@jira_service.password).to eq('password')
end
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq('password')
expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
it 'resets password if url changed, even if setter called multiple times' do
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
end
context 'when no password was previously set' do
before do
@jira_service = described_class.create(
project: project,
properties: {
url: 'http://jira.example.com/rest/api/2',
username: 'mic'
}
)
end
it 'saves password if new url is set together with password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq('password')
expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
end
end
describe 'description and title' do describe 'description and title' do
let(:title) { 'Jira One' } let(:title) { 'Jira One' }
let(:description) { 'Jira One issue tracker' } let(:description) { 'Jira One issue tracker' }
...@@ -539,7 +575,7 @@ describe JiraService do ...@@ -539,7 +575,7 @@ describe JiraService do
context 'when it is set in properties' do context 'when it is set in properties' do
it 'values from properties are returned' do it 'values from properties are returned' do
service = create(:jira_service, properties: properties) service = create(:jira_service, :without_properties_callback, properties: properties)
expect(service.title).to eq(title) expect(service.title).to eq(title)
expect(service.description).to eq(description) expect(service.description).to eq(description)
...@@ -602,8 +638,8 @@ describe JiraService do ...@@ -602,8 +638,8 @@ describe JiraService do
project = create(:project) project = create(:project)
service = project.create_jira_service(active: true) service = project.create_jira_service(active: true)
expect(service.properties['url']).to eq('http://jira.sample/projects/project_a') expect(service.url).to eq('http://jira.sample/projects/project_a')
expect(service.properties['api_url']).to eq('http://jira.sample/api') expect(service.api_url).to eq('http://jira.sample/api')
end end
end end
......
...@@ -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