Commit 21c2e88e authored by Mario de la Ossa's avatar Mario de la Ossa Committed by Alexandru Croitor

Setup multiple milestones

Adds migrations and relationships required to make multiple milestones
possible for issues, merge requests, and epics.
parent 72b6861d
......@@ -13,6 +13,7 @@ module Issuable
include CacheMarkdownField
include Participable
include Mentionable
include Milestoneable
include Subscribable
include StripAttribute
include Awardable
......@@ -56,7 +57,6 @@ module Issuable
belongs_to :author, class_name: 'User'
belongs_to :updated_by, class_name: 'User'
belongs_to :last_edited_by, class_name: 'User'
belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
def authors_loaded?
......@@ -89,18 +89,12 @@ module Issuable
# to avoid breaking the existing Issuables which may have their descriptions longer
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
validate :description_max_length_for_new_records_is_valid, on: :update
validate :milestone_is_valid
before_validation :truncate_description_on_import!
scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
......@@ -118,20 +112,6 @@ module Issuable
end
# rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
.where('milestone_releases.release_id IS NULL')
end
scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct
end
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) }
scope :join_project, -> { joins(:project) }
......@@ -164,10 +144,6 @@ module Issuable
private
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
end
def description_max_length_for_new_records_is_valid
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
......@@ -332,10 +308,6 @@ module Issuable
project
end
def milestone_available?
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
......@@ -482,13 +454,6 @@ module Issuable
def wipless_title_changed(old_title)
old_title != title
end
##
# Overridden on EE module
#
def supports_milestone?
respond_to?(:milestone_id)
end
end
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
......
# frozen_string_literal: true
# == Milestoneable concern
#
# Contains functionality related to objects that can be assigned Milestones
#
# Used by Issuable
#
module Milestoneable
extend ActiveSupport::Concern
included do
belongs_to :milestone
validate :milestone_is_valid
after_save :write_to_new_milestone_relationship
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
.where('milestone_releases.release_id IS NULL')
end
scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct
end
private
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
end
def write_to_new_milestone_relationship
self.milestones = [milestone].compact if supports_milestone? && saved_change_to_milestone_id?
end
end
def milestone_available?
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
##
# Overridden on EE module
#
def supports_milestone?
respond_to?(:milestone_id)
end
end
Milestoneable.prepend_if_ee('EE::Milestoneable')
......@@ -33,6 +33,9 @@ class Issue < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
has_many :issue_milestones
has_many :milestones, through: :issue_milestones
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
......
# frozen_string_literal: true
class IssueMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :issue
end
......@@ -35,6 +35,9 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_diffs
has_many :merge_request_milestones
has_many :milestones, through: :merge_request_milestones
has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
......
# frozen_string_literal: true
class MergeRequestMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :merge_request
end
......@@ -38,6 +38,9 @@ class Milestone < ApplicationRecord
has_many :merge_requests
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_milestones
has_many :merge_request_milestones
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
......
---
title: Setup storage for multiple milestones
merge_request: 22043
author:
type: added
# frozen_string_literal: true
class SupportMultipleMilestonesForIssues < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :issue_milestones, id: false do |t|
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
end
add_index :issue_milestones, [:issue_id, :milestone_id], unique: true
end
end
# frozen_string_literal: true
class SupportMultipleMilestonesForMergeRequests < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :merge_request_milestones, id: false do |t|
t.references :merge_request, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
end
add_index :merge_request_milestones, [:merge_request_id, :milestone_id], name: 'index_mrs_milestones_on_mr_id_and_milestone_id', unique: true
end
end
......@@ -2100,6 +2100,14 @@ ActiveRecord::Schema.define(version: 2019_12_18_225624) do
t.index ["issue_id"], name: "index_issue_metrics"
end
create_table "issue_milestones", id: false, force: :cascade do |t|
t.bigint "issue_id", null: false
t.bigint "milestone_id", null: false
t.index ["issue_id", "milestone_id"], name: "index_issue_milestones_on_issue_id_and_milestone_id", unique: true
t.index ["issue_id"], name: "index_issue_milestones_on_issue_id", unique: true
t.index ["milestone_id"], name: "index_issue_milestones_on_milestone_id"
end
create_table "issue_tracker_data", force: :cascade do |t|
t.integer "service_id", null: false
t.datetime_with_timezone "created_at", null: false
......@@ -2487,6 +2495,14 @@ ActiveRecord::Schema.define(version: 2019_12_18_225624) do
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
end
create_table "merge_request_milestones", id: false, force: :cascade do |t|
t.bigint "merge_request_id", null: false
t.bigint "milestone_id", null: false
t.index ["merge_request_id", "milestone_id"], name: "index_mrs_milestones_on_mr_id_and_milestone_id", unique: true
t.index ["merge_request_id"], name: "index_merge_request_milestones_on_merge_request_id", unique: true
t.index ["milestone_id"], name: "index_merge_request_milestones_on_milestone_id"
end
create_table "merge_request_user_mentions", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "note_id"
......@@ -4587,6 +4603,8 @@ ActiveRecord::Schema.define(version: 2019_12_18_225624) do
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issue_milestones", "issues", on_delete: :cascade
add_foreign_key "issue_milestones", "milestones", on_delete: :cascade
add_foreign_key "issue_tracker_data", "services", on_delete: :cascade
add_foreign_key "issue_user_mentions", "issues", on_delete: :cascade
add_foreign_key "issue_user_mentions", "notes", on_delete: :cascade
......@@ -4630,6 +4648,8 @@ ActiveRecord::Schema.define(version: 2019_12_18_225624) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
add_foreign_key "merge_request_milestones", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_milestones", "milestones", on_delete: :cascade
add_foreign_key "merge_request_user_mentions", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_user_mentions", "notes", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
......
......@@ -18,18 +18,6 @@ module EE
end
end
override :milestone_available?
def milestone_available?
return true if is_a?(Epic)
super
end
override :supports_milestone?
def supports_milestone?
super && !is_a?(Epic)
end
def supports_epic?
is_a?(Issue) && project.group
end
......
# frozen_string_literal: true
module EE
module Milestoneable
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :milestone_available?
def milestone_available?
# This is to avoid attempting to set milestone_id in an Epic to nil, which would cause an exception
# as Epic doesn't have milestone_id
return true if is_a?(Epic)
super
end
override :supports_milestone?
def supports_milestone?
super && !is_a?(Epic)
end
end
end
......@@ -39,37 +39,6 @@ describe EE::Issuable do
end
end
describe '#milestone_available?' do
context 'with Epic' do
let(:epic) { create(:epic) }
it 'returns true' do
expect(epic.milestone_available?).to be_truthy
end
end
context 'no Epic' do
let(:issue) { create(:issue) }
it 'returns false' do
expect(issue.milestone_available?).to be_falsy
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for epics" do
let(:epic) { build(:epic) }
it 'returns false' do
expect(epic.supports_milestone?).to be_falsy
end
end
end
describe '#matches_cross_reference_regex?' do
context "epic description with long path string" do
let(:mentionable) { build(:epic, description: "/a" * 50000) }
......
# frozen_string_literal: true
require 'spec_helper'
describe EE::Milestoneable do
describe '#milestone_available?' do
context 'no Epic' do
let(:issue) { create(:issue) }
it 'returns false' do
expect(issue.milestone_available?).to be_falsy
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for epics" do
let(:epic) { build(:epic) }
it 'returns false' do
expect(epic.supports_milestone?).to be_falsy
end
end
end
end
......@@ -24,6 +24,8 @@ tree:
- milestone:
- events:
- :push_event_payload
- issue_milestones:
- :milestone
- resource_label_events:
- label:
- :priorities
......@@ -57,6 +59,8 @@ tree:
- milestone:
- events:
- :push_event_payload
- merge_request_milestones:
- :milestone
- resource_label_events:
- label:
- :priorities
......@@ -202,6 +206,12 @@ excluded_attributes:
- :latest_merge_request_diff_id
- :head_pipeline_id
- :state_id
issue_milestones:
- :milestone_id
- :issue_id
merge_request_milestones:
- :milestone_id
- :merge_request_id
award_emoji:
- :awardable_id
statuses:
......
......@@ -6,6 +6,8 @@ issues:
- assignees
- updated_by
- milestone
- issue_milestones
- milestones
- notes
- resource_label_events
- sentry_issue
......@@ -77,6 +79,8 @@ milestone:
- boards
- milestone_releases
- releases
- issue_milestones
- merge_request_milestones
snippets:
- author
- project
......@@ -105,6 +109,8 @@ merge_requests:
- assignee
- updated_by
- milestone
- merge_request_milestones
- milestones
- notes
- resource_label_events
- label_links
......@@ -145,6 +151,12 @@ merge_requests:
- deployment_merge_requests
- deployments
- user_mentions
issue_milestones:
- milestone
- issue
merge_request_milestones:
- milestone
- merge_request
external_pull_requests:
- project
merge_request_diff:
......
......@@ -53,43 +53,6 @@ describe Issuable do
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
describe 'milestone' do
let(:project) { create(:project) }
let(:milestone_id) { create(:milestone, project: project).id }
let(:params) do
{
title: 'something',
project: project,
author: build(:user),
milestone_id: milestone_id
}
end
subject { issuable_class.new(params) }
context 'with correct params' do
it { is_expected.to be_valid }
end
context 'with empty string milestone' do
let(:milestone_id) { '' }
it { is_expected.to be_valid }
end
context 'with nil milestone id' do
let(:milestone_id) { nil }
it { is_expected.to be_valid }
end
context 'with a milestone id from another project' do
let(:milestone_id) { create(:milestone).id }
it { is_expected.to be_invalid }
end
end
end
describe "Scope" do
......@@ -141,48 +104,6 @@ describe Issuable do
end
end
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_issuable(milestone_id)
issuable_class.new(project: project, milestone_id: milestone_id)
end
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the the parent of the issue project group' do
parent = create(:group)
group.update(parent: parent)
milestone = create(:milestone, group: parent)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
end
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
......@@ -809,27 +730,6 @@ describe Issuable do
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for issues" do
let(:issue) { build(:issue, project: project) }
it 'returns true' do
expect(issue.supports_milestone?).to be_truthy
end
end
context "for merge requests" do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
it 'returns true' do
expect(merge_request.supports_milestone?).to be_truthy
end
end
end
describe '#matches_cross_reference_regex?' do
context "issue description with long path string" do
let(:mentionable) { build(:issue, description: "/a" * 50000) }
......@@ -854,91 +754,4 @@ describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
describe 'release scopes' do
let_it_be(:project) { create(:project) }
let(:forked_project) { fork_project(project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) }
let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
let(:mr_3) { create(:merge_request, source_project: project) }
let_it_be(:issue_items) { Issue.all }
let(:mr_items) { MergeRequest.all }
describe '#without_release' do
it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
expect(mr_items.without_release).to contain_exactly(mr_3)
end
end
describe '#any_release' do
it 'returns all issues or all mrs tied to a release' do
expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
end
end
describe '#with_release' do
it 'returns the issues tied to a specfic release' do
expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
it 'returns the mrs tied to a specific release' do
expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
end
context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do
expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end
context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do
expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end
end
context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do
expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end
end
end
context 'when there is no issue or mr under a specific release' do
it 'returns no issue or no mr' do
expect(issue_items.with_release('v4.0', project.id)).to be_empty
expect(mr_items.with_release('v4.0', project.id)).to be_empty
end
end
context 'when a non-existent release tag is passed in' do
it 'returns no issue or no mr' do
expect(issue_items.with_release('v999.0', project.id)).to be_empty
expect(mr_items.with_release('v999.0', project.id)).to be_empty
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Milestoneable do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
shared_examples_for 'an object that can be assigned a milestone' do
describe 'Validation' do
describe 'milestone' do
let(:project) { create(:project, :repository) }
let(:milestone_id) { milestone.id }
subject { milestoneable_class.new(params) }
context 'with correct params' do
it { is_expected.to be_valid }
end
context 'with empty string milestone' do
let(:milestone_id) { '' }
it { is_expected.to be_valid }
end
context 'with nil milestone id' do
let(:milestone_id) { nil }
it { is_expected.to be_valid }
end
context 'with a milestone id from another project' do
let(:milestone_id) { create(:milestone).id }
it { is_expected.to be_invalid }
end
context 'when valid and saving' do
it 'copies the value to the new milestones relationship' do
subject.save!
expect(subject.milestones).to match_array([milestone])
end
context 'with old values in milestones relationship' do
let(:old_milestone) { create(:milestone, project: project) }
before do
subject.milestone = old_milestone
subject.save!
end
it 'replaces old values' do
expect(subject.milestones).to match_array([old_milestone])
subject.milestone = milestone
subject.save!
expect(subject.milestones).to match_array([milestone])
end
it 'can nullify the milestone' do
expect(subject.milestones).to match_array([old_milestone])
subject.milestone = nil
subject.save!
expect(subject.milestones).to match_array([])
end
end
end
end
end
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_milestoneable(milestone_id)
milestoneable_class.new(project: project, milestone_id: milestone_id)
end
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the the parent of the issue project group' do
parent = create(:group)
group.update(parent: parent)
milestone = create(:milestone, group: parent)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for issues" do
let(:issue) { build(:issue, project: project) }
it 'returns true' do
expect(issue.supports_milestone?).to be_truthy
end
end
context "for merge requests" do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
it 'returns true' do
expect(merge_request.supports_milestone?).to be_truthy
end
end
end
describe 'release scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) }
let_it_be(:items) { Issue.all }
describe '#without_release' do
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
expect(items.without_release).to contain_exactly(issue_5, issue_6)
end
end
describe '#any_release' do
it 'returns all issues tied to a release' do
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
end
end
describe '#with_release' do
it 'returns the issues tied a specfic release' do
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end
context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end
end
context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end
end
end
context 'when there is no issue under a specific release' do
it 'returns no issue' do
expect(items.with_release('v4.0', project.id)).to be_empty
end
end
context 'when a non-existent release tag is passed in' do
it 'returns no issue' do
expect(items.with_release('v999.0', project.id)).to be_empty
end
end
end
end
context 'Issues' do
let(:milestoneable_class) { Issue }
let(:params) do
{
title: 'something',
project: project,
author: user,
milestone_id: milestone_id
}
end
it_behaves_like 'an object that can be assigned a milestone'
end
context 'MergeRequests' do
let(:milestoneable_class) { MergeRequest }
let(:params) do
{
title: 'something',
source_project: project,
target_project: project,
source_branch: 'feature',
target_branch: 'master',
author: user,
milestone_id: milestone_id
}
end
it_behaves_like 'an object that can be assigned a milestone'
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment