summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVinnie Okada <vokada@mrvinn.com>2014-10-01 10:31:13 -0500
committerVinnie Okada <vokada@mrvinn.com>2014-10-03 12:30:20 -0500
commit1b1ba6b0a51cda30df2fe68fa577f6045d187b86 (patch)
tree4d96b8319d32f31b04f45a30e66adf9e2c803ed1
parentc0bb3f5af200a457cd375b455528578d1c459e40 (diff)
downloadgitlab-ce-1b1ba6b0a51cda30df2fe68fa577f6045d187b86.tar.gz
Implement cross-project Markdown references
Enable linking to commits, merge requests, and issues in other projects by prepending a namespaced project path to the reference.
-rw-r--r--CHANGELOG1
-rw-r--r--doc/markdown/markdown.md6
-rw-r--r--lib/gitlab/markdown.rb90
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb100
4 files changed, 166 insertions, 31 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 14d2572f742..857a5bc9234 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -17,6 +17,7 @@ v 7.4.0
- Font Awesome 4.2 integration (Sullivan Senechal)
- Add Pushover service integration (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
- Fix creating new file via web editor
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 5627fd0659f..5c095ed1487 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -177,6 +177,12 @@ GFM will recognize the following:
- 1234567 : for commits
- \[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
## Headers
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index d346acf0d32..8380193deb3 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -108,15 +108,18 @@ module Gitlab
text
end
+ NAME_STR = '[a-zA-Z][a-zA-Z0-9_\-\.]*'
+ PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
+
REFERENCE_PATTERN = %r{
(?<prefix>\W)? # Prefix
( # 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-zA-Z\-]+-)?\d+) # Issue ID
- |!(?<merge_request>\d+) # MR ID
+ |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
+ |#{PROJ_STR}?!(?<merge_request>\d+) # MR 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
)
(?<suffix>\W)? # Suffix
@@ -127,38 +130,57 @@ module Gitlab
def parse_references(text, project = @project)
# parse reference links
text.gsub!(REFERENCE_PATTERN) do |match|
- prefix = $~[:prefix]
- suffix = $~[:suffix]
type = TYPES.select{|t| !$~[t].nil?}.first
- if type
- identifier = $~[type]
-
- # Avoid HTML entities
- if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
- match
- elsif ref_link = reference_link(type, identifier, project)
- "#{prefix}#{ref_link}#{suffix}"
- else
- match
- end
- else
- match
+ actual_project = project
+ project_prefix = nil
+ project_path = $LAST_MATCH_INFO[:project]
+ if project_path
+ actual_project = ::Project.find_with_namespace(project_path)
+ project_prefix = project_path
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 either +type+ or +project+ are nil, if the match
+ # string is an HTML entity, or if the reference is invalid.
+ def parse_result(match_info, type, project, project_prefix)
+ prefix = match_info[:prefix]
+ suffix = match_info[:suffix]
+
+ return nil if html_entity?(prefix, suffix) || project.nil? || type.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
+ # 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
#
# reference - Object reference ("@1234", "!567", etc.)
# identifier - Object identifier (Issue ID, SHA hash, etc.)
#
# Returns string rendered by the processing method
- def reference_link(type, identifier, project = @project)
- send("reference_#{type}", identifier, project)
+ def reference_link(type, identifier, project = @project, prefix_text = nil)
+ send("reference_#{type}", identifier, project, prefix_text)
end
- def reference_user(identifier, project = @project)
+ def reference_user(identifier, project = @project, _ = nil)
options = html_options.merge(
class: "gfm gfm-team_member #{html_options[:class]}"
)
@@ -170,17 +192,17 @@ module Gitlab
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.issue_exists? identifier
url = url_for_issue(identifier, project)
- title = title_for_issue(identifier)
+ title = title_for_issue(identifier, project)
options = html_options.merge(
title: "Issue: #{title}",
class: "gfm gfm-issue #{html_options[:class]}"
)
- link_to("##{identifier}", url, options)
+ link_to("#{prefix_text}##{identifier}", url, options)
end
else
config = Gitlab.config
@@ -191,18 +213,19 @@ module Gitlab
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)
options = html_options.merge(
title: "Merge Request: #{merge_request.title}",
class: "gfm gfm-merge_request #{html_options[:class]}"
)
url = project_merge_request_url(project, merge_request)
- link_to("!#{identifier}", url, options)
+ link_to("#{prefix_text}!#{identifier}", url, options)
end
end
- def reference_snippet(identifier, project = @project)
+ def reference_snippet(identifier, project = @project, _ = nil)
if snippet = project.snippets.find_by(id: identifier)
options = html_options.merge(
title: "Snippet: #{snippet.title}",
@@ -213,17 +236,22 @@ module Gitlab
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)
options = html_options.merge(
title: commit.link_title,
class: "gfm gfm-commit #{html_options[:class]}"
)
- link_to(identifier, project_commit_url(project, commit), options)
+ link_to(
+ "#{prefix_text}#{identifier}",
+ project_commit_url(project, commit),
+ options
+ )
end
end
- def reference_external_issue(identifier, issue_tracker, project = @project)
+ def reference_external_issue(identifier, issue_tracker, project = @project,
+ _ = nil)
url = url_for_issue(identifier, project)
title = issue_tracker['title']
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 6c865b1e079..da246ff1e0d 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -181,6 +181,74 @@ describe GitlabMarkdownHelper do
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)
+ 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
let(:object) { issue }
let(:reference) { "##{issue.iid}" }
@@ -188,6 +256,38 @@ describe GitlabMarkdownHelper do
include_examples 'referenced object'
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
let(:actual) { "Reference to JIRA-#{issue.iid}" }
let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" }