Commit bb929c21 authored by Marin Jankovski's avatar Marin Jankovski

Merge pull request #7933 from mr-vinn/cross-project-markdown

Implement cross-project Markdown references
parents 43be3fcb 8dce0cd2
...@@ -17,6 +17,7 @@ v 7.4.0 ...@@ -17,6 +17,7 @@ v 7.4.0
- Font Awesome 4.2 integration (Sullivan Senechal) - Font Awesome 4.2 integration (Sullivan Senechal)
- Add Pushover service integration (Sullivan Senechal) - Add Pushover service integration (Sullivan Senechal)
- Add select field type for services options (Sullivan Senechal) - Add select field type for services options (Sullivan Senechal)
- Add cross-project references to the Markdown parser (Vinnie Okada)
v 7.3.2 v 7.3.2
- Fix creating new file via web editor - Fix creating new file via web editor
......
...@@ -67,8 +67,10 @@ module Mentionable ...@@ -67,8 +67,10 @@ module Mentionable
def references(p = project, text = mentionable_text) def references(p = project, text = mentionable_text)
return [] if text.blank? return [] if text.blank?
ext = Gitlab::ReferenceExtractor.new ext = Gitlab::ReferenceExtractor.new
ext.analyze(text) ext.analyze(text, p)
(ext.issues_for(p) + ext.merge_requests_for(p) + ext.commits_for(p)).uniq - [local_reference] (ext.issues_for +
ext.merge_requests_for +
ext.commits_for).uniq - [local_reference]
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
......
...@@ -70,13 +70,17 @@ class Note < ActiveRecord::Base ...@@ -70,13 +70,17 @@ class Note < ActiveRecord::Base
) )
end end
# +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note. # +noteable+ was referenced from +mentioner+, by including GFM in either
# Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+. # +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference
# to +mentioner+.
def create_cross_reference_note(noteable, mentioner, author, project) def create_cross_reference_note(noteable, mentioner, author, project)
gfm_reference = mentioner_gfm_ref(noteable, mentioner, project)
note_options = { note_options = {
project: project, project: project,
author: author, author: author,
note: "_mentioned in #{mentioner.gfm_reference}_", note: "_mentioned in #{gfm_reference}_",
system: true system: true
} }
...@@ -163,12 +167,73 @@ class Note < ActiveRecord::Base ...@@ -163,12 +167,73 @@ class Note < ActiveRecord::Base
# Determine whether or not a cross-reference note already exists. # Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner) def cross_reference_exists?(noteable, mentioner)
where(noteable_id: noteable.id, system: true, note: "_mentioned in #{mentioner.gfm_reference}_").any? gfm_reference = mentioner_gfm_ref(noteable, mentioner)
where(['noteable_id = ? and system = ? and note like ?',
noteable.id, true, "_mentioned in #{gfm_reference}_"]).any?
end end
def search(query) def search(query)
where("note like :query", query: "%#{query}%") where("note like :query", query: "%#{query}%")
end end
private
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def mentioner_gfm_ref(noteable, mentioner, project = nil)
if mentioner.is_a?(Commit)
if project.nil?
return mentioner.gfm_reference.sub('commit ', 'commit %')
else
mentioning_project = project
end
else
mentioning_project = mentioner.project
end
noteable_project_id = noteable_project_id(noteable, mentioning_project)
full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
end
# Return the ID of the project that +noteable+ belongs to, or nil if
# +noteable+ is a commit and is not part of the project that owns
# +mentioner+.
def noteable_project_id(noteable, mentioning_project)
if noteable.is_a?(Commit)
if mentioning_project.repository.commit(noteable.id)
# The noteable commit belongs to the mentioner's project
mentioning_project.id
else
nil
end
else
noteable.project.id
end
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
if mentioning_project.id == noteable_project_id
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
end end
def commit_author def commit_author
......
...@@ -177,6 +177,12 @@ GFM will recognize the following: ...@@ -177,6 +177,12 @@ GFM will recognize the following:
- 1234567 : for commits - 1234567 : for commits
- \[file\](path/to/file) : for file references - \[file\](path/to/file) : for file references
GFM also recognizes references to commits, issues, and merge requests in other projects:
- namespace/project#123 : for issues
- namespace/project!123 : for merge requests
- namespace/project@1234567 : for commits
# Standard Markdown # Standard Markdown
## Headers ## Headers
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
md = ISSUE_CLOSING_REGEX.match(message) md = ISSUE_CLOSING_REGEX.match(message)
if md if md
extractor = Gitlab::ReferenceExtractor.new extractor = Gitlab::ReferenceExtractor.new
extractor.analyze(md[0]) extractor.analyze(md[0], project)
extractor.issues_for(project) extractor.issues_for(project)
else else
[] []
......
...@@ -108,15 +108,18 @@ module Gitlab ...@@ -108,15 +108,18 @@ module Gitlab
text text
end end
NAME_STR = '[a-zA-Z][a-zA-Z0-9_\-\.]*'
PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
REFERENCE_PATTERN = %r{ REFERENCE_PATTERN = %r{
(?<prefix>\W)? # Prefix (?<prefix>\W)? # Prefix
( # Reference ( # Reference
@(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name @(?<user>#{NAME_STR}) # User name
|(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
|\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
|!(?<merge_request>\d+) # MR ID |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
|\$(?<snippet>\d+) # Snippet ID |\$(?<snippet>\d+) # Snippet ID
|(?<commit>[\h]{6,40}) # Commit ID |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
|(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
) )
(?<suffix>\W)? # Suffix (?<suffix>\W)? # Suffix
...@@ -127,38 +130,59 @@ module Gitlab ...@@ -127,38 +130,59 @@ module Gitlab
def parse_references(text, project = @project) def parse_references(text, project = @project)
# parse reference links # parse reference links
text.gsub!(REFERENCE_PATTERN) do |match| text.gsub!(REFERENCE_PATTERN) do |match|
prefix = $~[:prefix]
suffix = $~[:suffix]
type = TYPES.select{|t| !$~[t].nil?}.first type = TYPES.select{|t| !$~[t].nil?}.first
if type actual_project = project
identifier = $~[type] project_prefix = nil
project_path = $LAST_MATCH_INFO[:project]
# Avoid HTML entities if project_path
if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' actual_project = ::Project.find_with_namespace(project_path)
match project_prefix = project_path
elsif ref_link = reference_link(type, identifier, project)
"#{prefix}#{ref_link}#{suffix}"
else
match
end
else
match
end end
parse_result($LAST_MATCH_INFO, type,
actual_project, project_prefix) || match
end
end
# Called from #parse_references. Attempts to build a gitlab reference
# link. Returns nil if +type+ is nil, if the match string is an HTML
# entity, if the reference is invalid, or if the matched text includes an
# invalid project path.
def parse_result(match_info, type, project, project_prefix)
prefix = match_info[:prefix]
suffix = match_info[:suffix]
return nil if html_entity?(prefix, suffix) || type.nil?
return nil if project.nil? && !project_prefix.nil?
identifier = match_info[type]
ref_link = reference_link(type, identifier, project, project_prefix)
if ref_link
"#{prefix}#{ref_link}#{suffix}"
else
nil
end end
end end
# Return true if the +prefix+ and +suffix+ indicate that the matched string
# is an HTML entity like &amp;
def html_entity?(prefix, suffix)
prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
end
# Private: Dispatches to a dedicated processing method based on reference # Private: Dispatches to a dedicated processing method based on reference
# #
# reference - Object reference ("@1234", "!567", etc.) # reference - Object reference ("@1234", "!567", etc.)
# identifier - Object identifier (Issue ID, SHA hash, etc.) # identifier - Object identifier (Issue ID, SHA hash, etc.)
# #
# Returns string rendered by the processing method # Returns string rendered by the processing method
def reference_link(type, identifier, project = @project) def reference_link(type, identifier, project = @project, prefix_text = nil)
send("reference_#{type}", identifier, project) send("reference_#{type}", identifier, project, prefix_text)
end end
def reference_user(identifier, project = @project) def reference_user(identifier, project = @project, _ = nil)
options = html_options.merge( options = html_options.merge(
class: "gfm gfm-team_member #{html_options[:class]}" class: "gfm gfm-team_member #{html_options[:class]}"
) )
...@@ -170,39 +194,41 @@ module Gitlab ...@@ -170,39 +194,41 @@ module Gitlab
end end
end end
def reference_issue(identifier, project = @project) def reference_issue(identifier, project = @project, prefix_text = nil)
if project.used_default_issues_tracker? || !external_issues_tracker_enabled? if project.used_default_issues_tracker? || !external_issues_tracker_enabled?
if project.issue_exists? identifier if project.issue_exists? identifier
url = url_for_issue(identifier, project) url = url_for_issue(identifier, project)
title = title_for_issue(identifier) title = title_for_issue(identifier, project)
options = html_options.merge( options = html_options.merge(
title: "Issue: #{title}", title: "Issue: #{title}",
class: "gfm gfm-issue #{html_options[:class]}" class: "gfm gfm-issue #{html_options[:class]}"
) )
link_to("##{identifier}", url, options) link_to("#{prefix_text}##{identifier}", url, options)
end end
else else
config = Gitlab.config config = Gitlab.config
external_issue_tracker = config.issues_tracker[project.issues_tracker] external_issue_tracker = config.issues_tracker[project.issues_tracker]
if external_issue_tracker.present? if external_issue_tracker.present?
reference_external_issue(identifier, external_issue_tracker, project) reference_external_issue(identifier, external_issue_tracker, project,
prefix_text)
end end
end end
end end
def reference_merge_request(identifier, project = @project) def reference_merge_request(identifier, project = @project,
prefix_text = nil)
if merge_request = project.merge_requests.find_by(iid: identifier) if merge_request = project.merge_requests.find_by(iid: identifier)
options = html_options.merge( options = html_options.merge(
title: "Merge Request: #{merge_request.title}", title: "Merge Request: #{merge_request.title}",
class: "gfm gfm-merge_request #{html_options[:class]}" class: "gfm gfm-merge_request #{html_options[:class]}"
) )
url = project_merge_request_url(project, merge_request) url = project_merge_request_url(project, merge_request)
link_to("!#{identifier}", url, options) link_to("#{prefix_text}!#{identifier}", url, options)
end end
end end
def reference_snippet(identifier, project = @project) def reference_snippet(identifier, project = @project, _ = nil)
if snippet = project.snippets.find_by(id: identifier) if snippet = project.snippets.find_by(id: identifier)
options = html_options.merge( options = html_options.merge(
title: "Snippet: #{snippet.title}", title: "Snippet: #{snippet.title}",
...@@ -213,17 +239,23 @@ module Gitlab ...@@ -213,17 +239,23 @@ module Gitlab
end end
end end
def reference_commit(identifier, project = @project) def reference_commit(identifier, project = @project, prefix_text = nil)
if project.valid_repo? && commit = project.repository.commit(identifier) if project.valid_repo? && commit = project.repository.commit(identifier)
options = html_options.merge( options = html_options.merge(
title: commit.link_title, title: commit.link_title,
class: "gfm gfm-commit #{html_options[:class]}" class: "gfm gfm-commit #{html_options[:class]}"
) )
link_to(identifier, project_commit_url(project, commit), options) prefix_text = "#{prefix_text}@" if prefix_text
link_to(
"#{prefix_text}#{identifier}",
project_commit_url(project, commit),
options
)
end end
end end
def reference_external_issue(identifier, issue_tracker, project = @project) def reference_external_issue(identifier, issue_tracker, project = @project,
prefix_text = nil)
url = url_for_issue(identifier, project) url = url_for_issue(identifier, project)
title = issue_tracker['title'] title = issue_tracker['title']
...@@ -231,7 +263,7 @@ module Gitlab ...@@ -231,7 +263,7 @@ module Gitlab
title: "Issue in #{title}", title: "Issue in #{title}",
class: "gfm gfm-issue #{html_options[:class]}" class: "gfm gfm-issue #{html_options[:class]}"
) )
link_to("##{identifier}", url, options) link_to("#{prefix_text}##{identifier}", url, options)
end end
end end
end end
...@@ -9,51 +9,63 @@ module Gitlab ...@@ -9,51 +9,63 @@ module Gitlab
@users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], [] @users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], []
end end
def analyze(string) def analyze(string, project)
parse_references(string.dup) parse_references(string.dup, project)
end end
# Given a valid project, resolve the extracted identifiers of the requested type to # Given a valid project, resolve the extracted identifiers of the requested type to
# model objects. # model objects.
def users_for(project) def users_for(project)
users.map do |identifier| users.map do |entry|
project.users.where(username: identifier).first project.users.where(username: entry[:id]).first
end.reject(&:nil?) end.reject(&:nil?)
end end
def issues_for(project) def issues_for(project = nil)
issues.map do |identifier| issues.map do |entry|
project.issues.where(iid: identifier).first if should_lookup?(project, entry[:project])
entry[:project].issues.where(iid: entry[:id]).first
end
end.reject(&:nil?) end.reject(&:nil?)
end end
def merge_requests_for(project) def merge_requests_for(project = nil)
merge_requests.map do |identifier| merge_requests.map do |entry|
project.merge_requests.where(iid: identifier).first if should_lookup?(project, entry[:project])
entry[:project].merge_requests.where(iid: entry[:id]).first
end
end.reject(&:nil?) end.reject(&:nil?)
end end
def snippets_for(project) def snippets_for(project)
snippets.map do |identifier| snippets.map do |entry|
project.snippets.where(id: identifier).first project.snippets.where(id: entry[:id]).first
end.reject(&:nil?) end.reject(&:nil?)
end end
def commits_for(project) def commits_for(project = nil)
repo = project.repository commits.map do |entry|
return [] if repo.nil? repo = entry[:project].repository if entry[:project]
if should_lookup?(project, entry[:project])
commits.map do |identifier| repo.commit(entry[:id]) if repo
repo.commit(identifier) end
end.reject(&:nil?) end.reject(&:nil?)
end end
private private
def reference_link(type, identifier, project) def reference_link(type, identifier, project, _)
# Append identifier to the appropriate collection. # Append identifier to the appropriate collection.
send("#{type}s") << identifier send("#{type}s") << { project: project, id: identifier }
end
def should_lookup?(project, entry_project)
if entry_project.nil?
false
else
project.nil? || project.id == entry_project.id
end
end end
end end
end end
...@@ -181,6 +181,76 @@ describe GitlabMarkdownHelper do ...@@ -181,6 +181,76 @@ describe GitlabMarkdownHelper do
end end
end end
# Shared examples for referencing an object in a different project
#
# Expects the following attributes to be available in the example group:
#
# - object - The object itself
# - reference - The object reference string (e.g., #1234, $1234, !1234)
# - other_project - The project that owns the target object
#
# Currently limited to Snippets, Issues and MergeRequests
shared_examples 'cross-project referenced object' do
let(:project_path) { @other_project.path_with_namespace }
let(:full_reference) { "#{project_path}#{reference}" }
let(:actual) { "Reference to #{full_reference}" }
let(:expected) do
if object.is_a?(Commit)
project_commit_path(@other_project, object)
else
polymorphic_path([@other_project, object])
end
end
it 'should link using a valid id' do
gfm(actual).should match(
/#{expected}.*#{Regexp.escape(full_reference)}/
)
end
it 'should link with adjacent text' do
# Wrap the reference in parenthesis
gfm(actual.gsub(full_reference, "(#{full_reference})")).should(
match(expected)
)
# Append some text to the end of the reference
gfm(actual.gsub(full_reference, "#{full_reference}, right?")).should(
match(expected)
)
end
it 'should keep whitespace intact' do
actual = "Referenced #{full_reference} already."
expected = /Referenced <a.+>[^\s]+<\/a> already/
gfm(actual).should match(expected)
end
it 'should not link with an invalid id' do
# Modify the reference string so it's still parsed, but is invalid
if object.is_a?(Commit)
reference.gsub!(/^(.).+$/, '\1' + '12345abcd')
else
reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
end
gfm(actual).should == actual
end
it 'should include a title attribute' do
if object.is_a?(Commit)
title = object.link_title
else
title = "#{object.class.to_s.titlecase}: #{object.title}"
end
gfm(actual).should match(/title="#{title}"/)
end
it 'should include standard gfm classes' do
css = object.class.to_s.underscore
gfm(actual).should match(/class="\s?gfm gfm-#{css}\s?"/)
end
end
describe "referencing an issue" do describe "referencing an issue" do
let(:object) { issue } let(:object) { issue }
let(:reference) { "##{issue.iid}" } let(:reference) { "##{issue.iid}" }
...@@ -188,6 +258,38 @@ describe GitlabMarkdownHelper do ...@@ -188,6 +258,38 @@ describe GitlabMarkdownHelper do
include_examples 'referenced object' include_examples 'referenced object'
end end
context 'cross-repo references' do
before(:all) do
@other_project = create(:project, :public)
@commit2 = @other_project.repository.commit
@issue2 = create(:issue, project: @other_project)
@merge_request2 = create(:merge_request,
source_project: @other_project,
target_project: @other_project)
end
describe 'referencing an issue in another project' do
let(:object) { @issue2 }
let(:reference) { "##{@issue2.iid}" }
include_examples 'cross-project referenced object'
end
describe 'referencing an merge request in another project' do
let(:object) { @merge_request2 }
let(:reference) { "!#{@merge_request2.iid}" }
include_examples 'cross-project referenced object'
end
describe 'referencing a commit in another project' do
let(:object) { @commit2 }
let(:reference) { "@#{@commit2.id}" }
include_examples 'cross-project referenced object'
end
end
describe "referencing a Jira issue" do describe "referencing a Jira issue" do
let(:actual) { "Reference to JIRA-#{issue.iid}" } let(:actual) { "Reference to JIRA-#{issue.iid}" }
let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" } let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" }
......
...@@ -2,45 +2,48 @@ require 'spec_helper' ...@@ -2,45 +2,48 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor do describe Gitlab::ReferenceExtractor do
it 'extracts username references' do it 'extracts username references' do
subject.analyze "this contains a @user reference" subject.analyze('this contains a @user reference', nil)
subject.users.should == ["user"] subject.users.should == [{ project: nil, id: 'user' }]
end end
it 'extracts issue references' do it 'extracts issue references' do
subject.analyze "this one talks about issue #1234" subject.analyze('this one talks about issue #1234', nil)
subject.issues.should == ["1234"] subject.issues.should == [{ project: nil, id: '1234' }]
end end
it 'extracts JIRA issue references' do it 'extracts JIRA issue references' do
Gitlab.config.gitlab.stub(:issues_tracker).and_return("jira") Gitlab.config.gitlab.stub(:issues_tracker).and_return('jira')
subject.analyze "this one talks about issue JIRA-1234" subject.analyze('this one talks about issue JIRA-1234', nil)
subject.issues.should == ["JIRA-1234"] subject.issues.should == [{ project: nil, id: 'JIRA-1234' }]
end end
it 'extracts merge request references' do it 'extracts merge request references' do
subject.analyze "and here's !43, a merge request" subject.analyze("and here's !43, a merge request", nil)
subject.merge_requests.should == ["43"] subject.merge_requests.should == [{ project: nil, id: '43' }]
end end
it 'extracts snippet ids' do it 'extracts snippet ids' do
subject.analyze "snippets like $12 get extracted as well" subject.analyze('snippets like $12 get extracted as well', nil)
subject.snippets.should == ["12"] subject.snippets.should == [{ project: nil, id: '12' }]
end end
it 'extracts commit shas' do it 'extracts commit shas' do
subject.analyze "commit shas 98cf0ae3 are pulled out as Strings" subject.analyze('commit shas 98cf0ae3 are pulled out as Strings', nil)
subject.commits.should == ["98cf0ae3"] subject.commits.should == [{ project: nil, id: '98cf0ae3' }]
end end
it 'extracts multiple references and preserves their order' do it 'extracts multiple references and preserves their order' do
subject.analyze "@me and @you both care about this" subject.analyze('@me and @you both care about this', nil)
subject.users.should == ["me", "you"] subject.users.should == [
{ project: nil, id: 'me' },
{ project: nil, id: 'you' }
]
end end
it 'leaves the original note unmodified' do it 'leaves the original note unmodified' do
text = "issue #123 is just the worst, @user" text = 'issue #123 is just the worst, @user'
subject.analyze text subject.analyze(text, nil)
text.should == "issue #123 is just the worst, @user" text.should == 'issue #123 is just the worst, @user'
end end
it 'handles all possible kinds of references' do it 'handles all possible kinds of references' do
...@@ -59,7 +62,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -59,7 +62,7 @@ describe Gitlab::ReferenceExtractor do
project.team << [@u_foo, :reporter] project.team << [@u_foo, :reporter]
project.team << [@u_bar, :guest] project.team << [@u_bar, :guest]
subject.analyze "@foo, @baduser, @bar, and @offteam" subject.analyze('@foo, @baduser, @bar, and @offteam', project)
subject.users_for(project).should == [@u_foo, @u_bar] subject.users_for(project).should == [@u_foo, @u_bar]
end end
...@@ -67,7 +70,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -67,7 +70,7 @@ describe Gitlab::ReferenceExtractor do
@i0 = create(:issue, project: project) @i0 = create(:issue, project: project)
@i1 = create(:issue, project: project) @i1 = create(:issue, project: project)
subject.analyze "##{@i0.iid}, ##{@i1.iid}, and #999." subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project)
subject.issues_for(project).should == [@i0, @i1] subject.issues_for(project).should == [@i0, @i1]
end end
...@@ -75,7 +78,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -75,7 +78,7 @@ describe Gitlab::ReferenceExtractor do
@m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa') @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa')
@m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb') @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb')
subject.analyze "!999, !#{@m1.iid}, and !#{@m0.iid}." subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project)
subject.merge_requests_for(project).should == [@m1, @m0] subject.merge_requests_for(project).should == [@m1, @m0]
end end
...@@ -84,14 +87,15 @@ describe Gitlab::ReferenceExtractor do ...@@ -84,14 +87,15 @@ describe Gitlab::ReferenceExtractor do
@s1 = create(:project_snippet, project: project) @s1 = create(:project_snippet, project: project)
@s2 = create(:project_snippet) @s2 = create(:project_snippet)
subject.analyze "$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}" subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project)
subject.snippets_for(project).should == [@s0, @s1] subject.snippets_for(project).should == [@s0, @s1]
end end
it 'accesses valid commits' do it 'accesses valid commits' do
commit = project.repository.commit("master") commit = project.repository.commit('master')
subject.analyze "this references commits #{commit.sha[0..6]} and 012345" subject.analyze("this references commits #{commit.sha[0..6]} and 012345",
project)
extracted = subject.commits_for(project) extracted = subject.commits_for(project)
extracted.should have(1).item extracted.should have(1).item
extracted[0].sha.should == commit.sha extracted[0].sha.should == commit.sha
......
...@@ -53,11 +53,23 @@ eos ...@@ -53,11 +53,23 @@ eos
describe '#closes_issues' do describe '#closes_issues' do
let(:issue) { create :issue, project: project } let(:issue) { create :issue, project: project }
let(:other_project) { create :project, :public }
let(:other_issue) { create :issue, project: other_project }
it 'detects issues that this commit is marked as closing' do it 'detects issues that this commit is marked as closing' do
commit.stub(issue_closing_regex: /^([Cc]loses|[Ff]ixes) #\d+/, safe_message: "Fixes ##{issue.iid}") stub_const('Gitlab::ClosingIssueExtractor::ISSUE_CLOSING_REGEX',
/Fixes #\d+/)
commit.stub(safe_message: "Fixes ##{issue.iid}")
commit.closes_issues(project).should == [issue] commit.closes_issues(project).should == [issue]
end end
it 'does not detect issues from other projects' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
stub_const('Gitlab::ClosingIssueExtractor::ISSUE_CLOSING_REGEX',
/^([Cc]loses|[Ff]ixes)/)
commit.stub(safe_message: "Fixes #{ext_ref}")
commit.closes_issues(project).should be_empty
end
end end
it_behaves_like 'a mentionable' do it_behaves_like 'a mentionable' do
......
...@@ -264,8 +264,8 @@ describe Note do ...@@ -264,8 +264,8 @@ describe Note do
let(:project) { create :project } let(:project) { create :project }
let(:author) { create :user } let(:author) { create :user }
let(:issue) { create :issue } let(:issue) { create :issue }
let(:commit0) { double 'commit0', gfm_reference: 'commit 123456' } let(:commit0) { project.repository.commit }
let(:commit1) { double 'commit1', gfm_reference: 'commit 654321' } let(:commit1) { project.repository.commit('HEAD~2') }
before do before do
Note.create_cross_reference_note(issue, commit0, author, project) Note.create_cross_reference_note(issue, commit0, author, project)
......
...@@ -14,13 +14,23 @@ def common_mentionable_setup ...@@ -14,13 +14,23 @@ def common_mentionable_setup
let(:mentioned_mr) { create :merge_request, :simple, source_project: mproject } let(:mentioned_mr) { create :merge_request, :simple, source_project: mproject }
let(:mentioned_commit) { double('commit', sha: '1234567890abcdef').as_null_object } let(:mentioned_commit) { double('commit', sha: '1234567890abcdef').as_null_object }
let(:ext_proj) { create :project, :public }
let(:ext_issue) { create :issue, project: ext_proj }
let(:other_ext_issue) { create :issue, project: ext_proj }
let(:ext_mr) { create :merge_request, :simple, source_project: ext_proj }
let(:ext_commit) { ext_proj.repository.commit }
# Override to add known commits to the repository stub. # Override to add known commits to the repository stub.
let(:extra_commits) { [] } let(:extra_commits) { [] }
# A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference
# to this string and place it in their +mentionable_text+. # to this string and place it in their +mentionable_text+.
let(:ref_string) do let(:ref_string) do
"mentions ##{mentioned_issue.iid} twice ##{mentioned_issue.iid}, !#{mentioned_mr.iid}, " + "mentions ##{mentioned_issue.iid} twice ##{mentioned_issue.iid}, " +
"!#{mentioned_mr.iid}, " +
"#{ext_proj.path_with_namespace}##{ext_issue.iid}, " +
"#{ext_proj.path_with_namespace}!#{ext_mr.iid}, " +
"#{ext_proj.path_with_namespace}@#{ext_commit.id[0..5]}, " +
"#{mentioned_commit.sha[0..5]} and itself as #{backref_text}" "#{mentioned_commit.sha[0..5]} and itself as #{backref_text}"
end end
...@@ -45,14 +55,20 @@ shared_examples 'a mentionable' do ...@@ -45,14 +55,20 @@ shared_examples 'a mentionable' do
# De-duplicate and omit itself # De-duplicate and omit itself
refs = subject.references(mproject) refs = subject.references(mproject)
refs.should have(3).items refs.should have(6).items
refs.should include(mentioned_issue) refs.should include(mentioned_issue)
refs.should include(mentioned_mr) refs.should include(mentioned_mr)
refs.should include(mentioned_commit) refs.should include(mentioned_commit)
refs.should include(ext_issue)
refs.should include(ext_mr)
refs.should include(ext_commit)
end end
it 'creates cross-reference notes' do it 'creates cross-reference notes' do
[mentioned_issue, mentioned_mr, mentioned_commit].each do |referenced| mentioned_objects = [mentioned_issue, mentioned_mr, mentioned_commit,
ext_issue, ext_mr, ext_commit]
mentioned_objects.each do |referenced|
Note.should_receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject) Note.should_receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject)
end end
...@@ -73,15 +89,25 @@ shared_examples 'an editable mentionable' do ...@@ -73,15 +89,25 @@ shared_examples 'an editable mentionable' do
it_behaves_like 'a mentionable' it_behaves_like 'a mentionable'
it 'creates new cross-reference notes when the mentionable text is edited' do it 'creates new cross-reference notes when the mentionable text is edited' do
new_text = "this text still mentions ##{mentioned_issue.iid} and #{mentioned_commit.sha[0..5]}, " + new_text = "still mentions ##{mentioned_issue.iid}, " +
"but now it mentions ##{other_issue.iid}, too." "#{mentioned_commit.sha[0..5]}, " +
"#{ext_issue.iid}, " +
"new refs: ##{other_issue.iid}, " +
"#{ext_proj.path_with_namespace}##{other_ext_issue.iid}"
[mentioned_issue, mentioned_commit].each do |oldref| [mentioned_issue, mentioned_commit, ext_issue].each do |oldref|
Note.should_not_receive(:create_cross_reference_note).with(oldref, subject.local_reference, Note.should_not_receive(:create_cross_reference_note).with(oldref, subject.local_reference,
mauthor, mproject) mauthor, mproject)
end end
Note.should_receive(:create_cross_reference_note).with(other_issue, subject.local_reference, mauthor, mproject) [other_issue, other_ext_issue].each do |newref|
Note.should_receive(:create_cross_reference_note).with(
newref,
subject.local_reference,
mauthor,
mproject
)
end
subject.save subject.save
set_mentionable_text.call(new_text) set_mentionable_text.call(new_text)
......
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