Commit 149528f4 authored by Sean McGivern's avatar Sean McGivern

Support references to group milestones

Group milestones can only be referred to by name, not IID. They also do not
support cross-project references.
parent 03b816f3
...@@ -47,14 +47,6 @@ module GitlabRoutingHelper ...@@ -47,14 +47,6 @@ module GitlabRoutingHelper
project_pipeline_path(pipeline.project, pipeline.id, *args) project_pipeline_path(pipeline.project, pipeline.id, *args)
end end
def milestone_path(entity, *args)
if entity.is_group_milestone?
group_milestone_path(entity.group, entity, *args)
elsif entity.is_project_milestone?
project_milestone_path(entity.project, entity, *args)
end
end
def issue_url(entity, *args) def issue_url(entity, *args)
project_issue_url(entity.project, entity, *args) project_issue_url(entity.project, entity, *args)
end end
...@@ -67,14 +59,6 @@ module GitlabRoutingHelper ...@@ -67,14 +59,6 @@ module GitlabRoutingHelper
project_pipeline_url(pipeline.project, pipeline.id, *args) project_pipeline_url(pipeline.project, pipeline.id, *args)
end end
def milestone_url(entity, *args)
if entity.is_group_milestone?
group_milestone_url(entity.group, entity, *args)
elsif entity.is_project_milestone?
project_milestone_url(entity.project, entity, *args)
end
end
def pipeline_job_url(pipeline, build, *args) def pipeline_job_url(pipeline, build, *args)
project_job_url(pipeline.project, build.id, *args) project_job_url(pipeline.project, build.id, *args)
end end
......
module MilestonesRoutingHelper
def milestone_path(milestone, *args)
if milestone.is_group_milestone?
group_milestone_path(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
project_milestone_path(milestone.project, milestone, *args)
end
end
def milestone_url(milestone, *args)
if milestone.is_group_milestone?
group_milestone_url(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
project_milestone_url(milestone.project, milestone, *args)
end
end
end
...@@ -149,7 +149,9 @@ class Milestone < ActiveRecord::Base ...@@ -149,7 +149,9 @@ class Milestone < ActiveRecord::Base
end end
## ##
# Returns the String necessary to reference this Milestone in Markdown # Returns the String necessary to reference this Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
# references.
# #
# format - Symbol format to use (default: :iid, optional: :name) # format - Symbol format to use (default: :iid, optional: :name)
# #
...@@ -161,12 +163,16 @@ class Milestone < ActiveRecord::Base ...@@ -161,12 +163,16 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
# #
def to_reference(from_project = nil, format: :iid, full: false) def to_reference(from_project = nil, format: :iid, full: false)
return if is_group_milestone? return if is_group_milestone? && format != :name
format_reference = milestone_format_reference(format) format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
"#{project.to_reference(from_project, full: full)}#{reference}" if project
"#{project.to_reference(from_project, full: full)}#{reference}"
else
reference
end
end end
def reference_link_text(from_project = nil) def reference_link_text(from_project = nil)
......
...@@ -181,7 +181,11 @@ module Gitlab ...@@ -181,7 +181,11 @@ module Gitlab
end end
end end
# We add the MilestonesRoutingHelper because we know that this does not
# conflict with the methods defined in `project_url_helpers`, and we want
# these methods available in the same places.
Gitlab::Routing.add_helpers(project_url_helpers) Gitlab::Routing.add_helpers(project_url_helpers)
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end end
end end
end end
...@@ -248,7 +248,7 @@ GFM will recognize the following: ...@@ -248,7 +248,7 @@ GFM will recognize the following:
| `~123` | label by ID | | `~123` | label by ID |
| `~bug` | one-word label by name | | `~bug` | one-word label by name |
| `~"feature request"` | multi-word label by name | | `~"feature request"` | multi-word label by name |
| `%123` | milestone by ID | | `%123` | project milestone by ID |
| `%v1.23` | one-word milestone by name | | `%v1.23` | one-word milestone by name |
| `%"release candidate"` | multi-word milestone by name | | `%"release candidate"` | multi-word milestone by name |
| `9ba12248` | specific commit | | `9ba12248` | specific commit |
...@@ -262,7 +262,7 @@ GFM also recognizes certain cross-project references: ...@@ -262,7 +262,7 @@ GFM also recognizes certain cross-project references:
|:----------------------------------------|:------------------------| |:----------------------------------------|:------------------------|
| `namespace/project#123` | issue | | `namespace/project#123` | issue |
| `namespace/project!123` | merge request | | `namespace/project!123` | merge request |
| `namespace/project%123` | milestone | | `namespace/project%123` | project milestone |
| `namespace/project$123` | snippet | | `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison | | `namespace/project@9ba12248...b19a04f5` | commit range comparison |
...@@ -274,7 +274,7 @@ It also has a shorthand version to reference other projects from the same namesp ...@@ -274,7 +274,7 @@ It also has a shorthand version to reference other projects from the same namesp
|:------------------------------|:------------------------| |:------------------------------|:------------------------|
| `project#123` | issue | | `project#123` | issue |
| `project!123` | merge request | | `project!123` | merge request |
| `project%123` | milestone | | `project%123` | project milestone |
| `project$123` | snippet | | `project$123` | snippet |
| `project@9ba12248` | specific commit | | `project@9ba12248` | specific commit |
| `project@9ba12248...b19a04f5` | commit range comparison | | `project@9ba12248...b19a04f5` | commit range comparison |
......
...@@ -59,6 +59,12 @@ module Banzai ...@@ -59,6 +59,12 @@ module Banzai
# Example: project.merge_requests.find # Example: project.merge_requests.find
end end
# Override if the link reference pattern produces a different ID (global
# ID vs internal ID, for instance) to the regular reference pattern.
def find_object_from_link(project, id)
find_object(project, id)
end
def find_object_cached(project, id) def find_object_cached(project, id)
if RequestStore.active? if RequestStore.active?
cache = find_objects_cache[object_class][project.id] cache = find_objects_cache[object_class][project.id]
...@@ -69,6 +75,16 @@ module Banzai ...@@ -69,6 +75,16 @@ module Banzai
end end
end end
def find_object_from_link_cached(project, id)
if RequestStore.active?
cache = find_objects_from_link_cache[object_class][project.id]
get_or_set_cache(cache, id) { find_object_from_link(project, id) }
else
find_object_from_link(project, id)
end
end
def project_from_ref_cached(ref) def project_from_ref_cached(ref)
if RequestStore.active? if RequestStore.active?
cache = project_refs_cache cache = project_refs_cache
...@@ -120,7 +136,7 @@ module Banzai ...@@ -120,7 +136,7 @@ module Banzai
if link == inner_html && inner_html =~ /\A#{link_pattern}/ if link == inner_html && inner_html =~ /\A#{link_pattern}/
replace_link_node_with_text(node, link) do replace_link_node_with_text(node, link) do
object_link_filter(inner_html, link_pattern) object_link_filter(inner_html, link_pattern, link_reference: true)
end end
next next
...@@ -128,7 +144,7 @@ module Banzai ...@@ -128,7 +144,7 @@ module Banzai
if link =~ /\A#{link_pattern}\z/ if link =~ /\A#{link_pattern}\z/
replace_link_node_with_href(node, link) do replace_link_node_with_href(node, link) do
object_link_filter(link, link_pattern, link_content: inner_html) object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end end
next next
...@@ -146,15 +162,26 @@ module Banzai ...@@ -146,15 +162,26 @@ module Banzai
# text - String text to replace references in. # text - String text to replace references in.
# pattern - Reference pattern to match against. # pattern - Reference pattern to match against.
# link_content - Original content of the link being replaced. # link_content - Original content of the link being replaced.
# link_reference - True if this was using the link reference pattern,
# false otherwise.
# #
# Returns a String with references replaced with links. All links # Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
project_path = full_project_path(namespace_ref, project_ref) project_path = full_project_path(namespace_ref, project_ref)
project = project_from_ref_cached(project_path) project = project_from_ref_cached(project_path)
if project && object = find_object_cached(project, id) if project
object =
if link_reference
find_object_from_link_cached(project, id)
else
find_object_cached(project, id)
end
end
if object
title = object_link_title(object) title = object_link_title(object)
klass = reference_class(object_sym) klass = reference_class(object_sym)
...@@ -303,6 +330,12 @@ module Banzai ...@@ -303,6 +330,12 @@ module Banzai
end end
end end
def find_objects_from_link_cache
RequestStore[:banzai_find_objects_from_link_cache] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} }
end
end
def url_for_object_cache def url_for_object_cache
RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key| RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} } hash[key] = Hash.new { |h, k| h[k] = {} }
......
...@@ -8,8 +8,15 @@ module Banzai ...@@ -8,8 +8,15 @@ module Banzai
Milestone Milestone
end end
# Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate
# between group and project milestones.
def find_object(project, id) def find_object(project, id)
project.milestones.find_by(iid: id) find_milestone_with_finder(project, id: id)
end
def find_object_from_link(project, iid)
find_milestone_with_finder(project, iid: iid)
end end
def references_in(text, pattern = Milestone.reference_pattern) def references_in(text, pattern = Milestone.reference_pattern)
...@@ -22,7 +29,7 @@ module Banzai ...@@ -22,7 +29,7 @@ module Banzai
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone if milestone
yield match, milestone.iid, $~[:project], $~[:namespace], $~ yield match, milestone.id, $~[:project], $~[:namespace], $~
else else
match match
end end
...@@ -36,7 +43,8 @@ module Banzai ...@@ -36,7 +43,8 @@ module Banzai
return unless project return unless project
milestone_params = milestone_params(milestone_id, milestone_name) milestone_params = milestone_params(milestone_id, milestone_name)
project.milestones.find_by(milestone_params)
find_milestone_with_finder(project, milestone_params)
end end
def milestone_params(iid, name) def milestone_params(iid, name)
...@@ -47,15 +55,27 @@ module Banzai ...@@ -47,15 +55,27 @@ module Banzai
end end
end end
def find_milestone_with_finder(project, params)
finder_params = { project_ids: [project.id], order: nil }
# We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones.
if project.group && !params[:iid]
finder_params[:group_ids] = [project.group.id]
end
MilestonesFinder.new(finder_params).execute.find_by(params)
end
def url_for_object(milestone, project) def url_for_object(milestone, project)
h = Gitlab::Routing.url_helpers Gitlab::Routing
h.project_milestone_url(project, milestone, .url_helpers
only_path: context[:only_path]) .milestone_url(milestone, only_path: context[:only_path])
end end
def object_link_text(object, matches) def object_link_text(object, matches)
milestone_link = escape_once(super) milestone_link = escape_once(super)
reference = object.project.to_reference(project) reference = object.project&.to_reference(project)
if reference.present? if reference.present?
"#{milestone_link} <i>in #{reference}</i>".html_safe "#{milestone_link} <i>in #{reference}</i>".html_safe
......
...@@ -227,8 +227,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e ...@@ -227,8 +227,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Milestone in another project: <%= xmilestone.to_reference(project) %> - Milestone in another project: <%= xmilestone.to_reference(project) %>
- Ignored in code: `<%= simple_milestone.to_reference %>` - Ignored in code: `<%= simple_milestone.to_reference %>`
- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) - Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
- Milestone by URL: <%= urls.project_milestone_url(milestone.project, milestone) %> - Milestone by URL: <%= urls.milestone_url(milestone) %>
- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) - Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
- Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %>
- Group milestone by name in quotes: <%= group_milestone.to_reference(format: :name) %>
- Group milestone by URL is ignore: <%= urls.milestone_url(group_milestone) %>
### Task Lists ### Task Lists
......
...@@ -63,44 +63,4 @@ describe GitlabRoutingHelper do ...@@ -63,44 +63,4 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) } it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end end
end end
describe '#milestone_path' do
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
it 'links to the group milestone page' do
expect(milestone_path(milestone))
.to eq(group_milestone_path(group, milestone))
end
end
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
it 'links to the project milestone page' do
expect(milestone_path(milestone))
.to eq(project_milestone_path(project, milestone))
end
end
end
describe '#milestone_url' do
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
it 'links to the group milestone page' do
expect(milestone_url(milestone))
.to eq(group_milestone_url(group, milestone))
end
end
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
it 'links to the project milestone page' do
expect(milestone_url(milestone))
.to eq(project_milestone_url(project, milestone))
end
end
end
end end
require 'spec_helper'
describe MilestonesRoutingHelper do
let(:project) { build_stubbed(:project) }
let(:group) { build_stubbed(:group) }
describe '#milestone_path' do
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
it 'links to the group milestone page' do
expect(milestone_path(milestone))
.to eq(group_milestone_path(group, milestone))
end
end
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
it 'links to the project milestone page' do
expect(milestone_path(milestone))
.to eq(project_milestone_path(project, milestone))
end
end
end
describe '#milestone_url' do
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
it 'links to the group milestone page' do
expect(milestone_url(milestone))
.to eq(group_milestone_url(group, milestone))
end
end
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
it 'links to the project milestone page' do
expect(milestone_url(milestone))
.to eq(project_milestone_url(project, milestone))
end
end
end
end
...@@ -230,16 +230,40 @@ describe Milestone do ...@@ -230,16 +230,40 @@ describe Milestone do
end end
describe '#to_reference' do describe '#to_reference' do
let(:project) { build(:project, name: 'sample-project') } let(:group) { build_stubbed(:group) }
let(:milestone) { build(:milestone, iid: 1, project: project) } let(:project) { build_stubbed(:project, name: 'sample-project') }
let(:another_project) { build_stubbed(:project, name: 'another-project', namespace: project.namespace) }
context 'for a project milestone' do
let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
it 'returns a String reference to the object' do
expect(milestone.to_reference).to eq '%1'
end
it 'returns a reference by name when the format is set to :name' do
expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
end
it 'returns a String reference to the object' do it 'supports a cross-project reference' do
expect(milestone.to_reference).to eq "%1" expect(milestone.to_reference(another_project)).to eq 'sample-project%1'
end
end end
it 'supports a cross-project reference' do context 'for a group milestone' do
another_project = build(:project, name: 'another-project', namespace: project.namespace) let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
expect(milestone.to_reference(another_project)).to eq "sample-project%1"
it 'returns nil with the default format' do
expect(milestone.to_reference).to be_nil
end
it 'returns a reference by name when the format is set to :name' do
expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
end
it 'does not supports cross-project references' do
expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
end
end end
end end
......
...@@ -23,7 +23,7 @@ class MarkdownFeature ...@@ -23,7 +23,7 @@ class MarkdownFeature
# Direct references ---------------------------------------------------------- # Direct references ----------------------------------------------------------
def project def project
@project ||= create(:project, :repository).tap do |project| @project ||= create(:project, :repository, group: group).tap do |project|
project.team << [user, :master] project.team << [user, :master]
end end
end end
...@@ -75,6 +75,10 @@ class MarkdownFeature ...@@ -75,6 +75,10 @@ class MarkdownFeature
@milestone ||= create(:milestone, name: 'next goal', project: project) @milestone ||= create(:milestone, name: 'next goal', project: project)
end end
def group_milestone
@group_milestone ||= create(:milestone, name: 'group-milestone', group: group)
end
# Cross-references ----------------------------------------------------------- # Cross-references -----------------------------------------------------------
def xproject def xproject
......
...@@ -155,7 +155,7 @@ module MarkdownMatchers ...@@ -155,7 +155,7 @@ module MarkdownMatchers
set_default_markdown_messages set_default_markdown_messages
match do |actual| match do |actual|
expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6) expect(actual).to have_selector('a.gfm.gfm-milestone', count: 8)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment