diff --git a/Gemfile b/Gemfile index 78af7f5db69a8c290e73a9c40ba881b5dc695973..94e2129f3c6f6be52bfd67420663763c306eff27 100644 --- a/Gemfile +++ b/Gemfile @@ -10,9 +10,6 @@ end gem "rails", "~> 4.1.0" -# Make links from text -gem 'rails_autolink', '~> 1.1' - # Default values for AR models gem "default_value_for", "~> 3.0.0" diff --git a/Gemfile.lock b/Gemfile.lock index bbc5639c84fa0735cbe8e78f4fd4c879bc791f4c..80ae41dc8fcddda913bee0f3d86320f502cd02de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -448,8 +448,6 @@ GEM sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - rails_autolink (1.1.6) - rails (> 3.1) railties (4.1.9) actionpack (= 4.1.9) activesupport (= 4.1.9) @@ -779,7 +777,6 @@ DEPENDENCIES rack-mini-profiler rack-oauth2 (~> 1.0.5) rails (~> 4.1.0) - rails_autolink (~> 1.1) raphael-rails (~> 2.1.2) rb-fsevent rb-inotify diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 12489ccc2d70abdd2ef1f7a50f6b249590efefd2..b93ea0f020eee0d1805919cea4498f2e628218ee 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,14 +48,16 @@ } .project-home-desc { + color: $gray; + float: left; font-size: 16px; line-height: 1.3; margin-right: 250px; - } - .project-home-desc { - float: left; - color: $gray; + // Render Markdown-generated HTML inline for this block + p { + display: inline; + } } } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 89dcdf57798251e0b70a8c91b133ffaf04f3279a..a539ec49f7afe2be25affd9fabf7576e824622e0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -279,10 +279,6 @@ def add_nofollow(link, html_options = {}) html_options end - def escaped_autolink(text) - auto_link ERB::Util.html_escape(text), link: :urls - end - def promo_host 'about.gitlab.com' end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1678311141e618ff54b76f88eb24641919b3e943..0687840af395ac92db8665205afd3f27f9710d4f 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,7 +11,7 @@ @#{@group.path} - if @group.description.present? .description - = escaped_autolink(@group.description) + = markdown(@group.description, pipeline: :description) %hr = render 'shared/show_aside' diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index f9cdda4a3babb3d2e93e6fcfe287bd42fba3bb63..076afb11a9d6235237e3a1f7f1c162ff849f4eee 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? - = escaped_autolink(@project.description) + = markdown(@project.description, pipeline: :description) - if can?(current_user, :admin_project, @project) – = link_to 'Edit', edit_namespace_project_path diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 5db1566f55deac9b9fb63c22606caf1950dd4736..fa9c0975bb8271775a272c03b7dbb860522367d1 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -57,6 +57,9 @@ def gfm_with_options(text, options = {}, html_options = {}) pipeline = HTML::Pipeline.new(filters) context = { + # SanitizationFilter + pipeline: options[:pipeline], + # EmojiFilter asset_root: Gitlab.config.gitlab.url, asset_host: Gitlab::Application.config.asset_host, diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index 88781fea0c8d96460dd32d0105ff44ccecf1aaab..74b3a8d274f62d6073c3a41bd5610ac9836567db 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -8,33 +8,54 @@ module Markdown # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter def whitelist - whitelist = super + # Descriptions are more heavily sanitized, allowing only a few elements. + # See http://git.io/vkuAN + if pipeline == :description + whitelist = LIMITED + whitelist[:elements] -= %w(pre code img ol ul li) + else + whitelist = super + end + + customize_whitelist(whitelist) + + whitelist + end + private + + def pipeline + context[:pipeline] || :default + end + + def customized?(transformers) + transformers.last.source_location[0] == __FILE__ + end + + def customize_whitelist(whitelist) # Only push these customizations once - unless customized?(whitelist[:transformers]) - # Allow code highlighting - whitelist[:attributes]['pre'] = %w(class) - whitelist[:attributes]['span'] = %w(class) + return if customized?(whitelist[:transformers]) - # Allow table alignment - whitelist[:attributes]['th'] = %w(style) - whitelist[:attributes]['td'] = %w(style) + # Allow code highlighting + whitelist[:attributes]['pre'] = %w(class) + whitelist[:attributes]['span'] = %w(class) - # Allow span elements - whitelist[:elements].push('span') + # Allow table alignment + whitelist[:attributes]['th'] = %w(style) + whitelist[:attributes]['td'] = %w(style) - # Remove `rel` attribute from `a` elements - whitelist[:transformers].push(remove_rel) + # Allow span elements + whitelist[:elements].push('span') - # Remove `class` attribute from non-highlight spans - whitelist[:transformers].push(clean_spans) - end + # Remove `rel` attribute from `a` elements + whitelist[:transformers].push(remove_rel) + + # Remove `class` attribute from non-highlight spans + whitelist[:transformers].push(clean_spans) whitelist end - private - def remove_rel lambda do |env| if env[:node_name] == 'a' @@ -53,10 +74,6 @@ def clean_spans end end end - - def customized?(transformers) - transformers.last.source_location[0] == __FILE__ - end end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..edc1c63a0aab3f90f38786fc79f9d8063791f6b7 --- /dev/null +++ b/spec/features/groups_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +feature 'Group' do + describe 'description' do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + before do + login_as(:admin) + end + + it 'parses Markdown' do + group.update_attribute(:description, 'This is **my** group') + visit path + expect(page).to have_css('.description > p > strong') + end + + it 'passes through html-pipeline' do + group.update_attribute(:description, 'This group is the :poop:') + visit path + expect(page).to have_css('.description > p > img') + end + + it 'sanitizes unwanted tags' do + group.update_attribute(:description, '# Group Description') + visit path + expect(page).not_to have_css('.description h1') + end + + it 'permits `rel` attribute on links' do + group.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.description a[rel]') + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index ee1b3bf749d1f7ec53b6735f5e181017b7ba0c0e..902968cebcb6924bec6ced442c707f7840281942 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -18,11 +18,13 @@ # -> `gfm_with_options` helper # -> HTML::Pipeline # -> Sanitize +# -> RelativeLink # -> Emoji # -> Table of Contents # -> Autolinks # -> Rinku (http, https, ftp) # -> Other schemes +# -> ExternalLink # -> References # -> TaskList # -> `html_safe` diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index cae11be7cdd32b06c30738d919ae1e077e5ba681..56523f6e1a8e70d18b8eb04d690c075c84d8784f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,24 +1,56 @@ require 'spec_helper' -describe "Projects", feature: true, js: true do - before { login_as :user } +feature 'Project' do + describe 'description' do + let(:project) { create(:project) } + let(:path) { namespace_project_path(project.namespace, project) } - describe "DELETE /projects/:id" do before do - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :master] - visit edit_namespace_project_path(@project.namespace, @project) + login_as(:admin) end - it "should remove project" do + it 'parses Markdown' do + project.update_attribute(:description, 'This is **my** project') + visit path + expect(page).to have_css('.project-home-desc > p > strong') + end + + it 'passes through html-pipeline' do + project.update_attribute(:description, 'This project is the :poop:') + visit path + expect(page).to have_css('.project-home-desc > p > img') + end + + it 'sanitizes unwanted tags' do + project.update_attribute(:description, '# Project Description') + visit path + expect(page).not_to have_css('.project-home-desc h1') + end + + it 'permits `rel` attribute on links' do + project.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.project-home-desc a[rel]') + end + end + + describe 'removal', js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + login_with(user) + project.team << [user, :master] + visit edit_namespace_project_path(project.namespace, project) + end + + it 'should remove project' do expect { remove_project }.to change {Project.count}.by(-1) end it 'should delete the project from disk' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{@project.path_with_namespace}/) - ).twice + expect(GitlabShellWorker).to receive(:perform_async). + with(:remove_repository, /#{project.path_with_namespace}/).twice remove_project end @@ -26,7 +58,7 @@ def remove_project click_link "Remove project" - fill_in 'confirm_name_input', with: @project.path + fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end end diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb index 0bbdc11a9798f6b82c3a7f8632ac63e4f6fe09f8..a14cb2da089cdc1fe4b36e449ee882189d54bc6b 100644 --- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb +++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb @@ -2,11 +2,9 @@ module Gitlab::Markdown describe AutolinkFilter do - let(:link) { 'http://about.gitlab.com/' } + include FilterSpecHelper - def filter(html, options = {}) - described_class.call(html, options) - end + let(:link) { 'http://about.gitlab.com/' } it 'does nothing when :autolink is false' do exp = act = link diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index d3695ee46d0e6297cdb225f3b5d73eb6d0a68132..e8391cc7aca045f021c4087bc9047c124e7834ad 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe CommitRangeReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit1) { project.commit } diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index a0d2cd7e22b82786ec98062688d6db14132fa998..a10d43c9a027d82ed6e53e713e50da1d35505338 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe CommitReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit) { project.commit } diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb index 18d55c4818fba947685b17cfa7cf24434978fa41..11efd9bb4cd33f36205fa7d7d02d7d018ff3f42e 100644 --- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb +++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb @@ -2,9 +2,7 @@ module Gitlab::Markdown describe EmojiFilter do - def filter(html, contexts = {}) - described_class.call(html, contexts) - end + include FilterSpecHelper before do ActionController::Base.asset_host = 'https://foo.com' diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb index bf9409589faad95023333cf09b027b11ba38ff67..f16095bc2b21b386261b9a926f1380bbe93b4ba1 100644 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe ExternalIssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb index c2ff4f80a42b4a0348aa898271c67c1c9ef8a2e2..a040b34577b958c033bfa8fc14d6d14877192937 100644 --- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_link_filter_spec.rb @@ -2,9 +2,7 @@ module Gitlab::Markdown describe ExternalLinkFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper it 'ignores elements without an href attribute' do exp = act = %q(<a id="ignored">Ignore Me</a>) diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index a838d7570c84641e4f6eb2dc7a248745e80d076f..fa43d33794d5d76c6ff4206a0449e7a7f6c4e8fe 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe IssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index 41987f57bca4dae9d4fb49b3f4406506de31156b..cf3337b1ba1abebbeea49f062aa013e2def7667b 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -3,7 +3,7 @@ module Gitlab::Markdown describe LabelReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:label) { create(:label, project: project) } diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 6aeb10936022ca2af549a027dd0b1c4518ae8aac..5945302a2da33ed5a153837031315be2cd3a6e24 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe MergeRequestReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:merge) { create(:merge_request, source_project: project) } diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb index 4a1aa766149abd400db56a371a4a48cbaf8e1c73..e50c82d0b3c44fa38625edb59f8999e0f6024290 100644 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -2,9 +2,7 @@ module Gitlab::Markdown describe SanitizationFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper describe 'default whitelist' do it 'sanitizes tags that are not whitelisted' do @@ -42,6 +40,13 @@ def filter(html, options = {}) end describe 'custom whitelist' do + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 4 + end + it 'allows syntax highlighting' do exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} expect(filter(act).to_html).to eq exp @@ -87,5 +92,27 @@ def filter(html, options = {}) expect(doc.at_css('a')['href']).to be_nil end end + + context 'when pipeline is :description' do + it 'uses a stricter whitelist' do + doc = filter('<h1>Description</h1>', pipeline: :description) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html).to eq exp + end + end + end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 07ece66e9034aa3dfe675490d5b5a4bbf6f5c029..38619a3c07f65471452cdb346946669eb7fd2184 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe SnippetReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:snippet) { create(:project_snippet, project: project) } diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb index f383a5850d5419ca307b45f317225d31f993d938..ddf583a72c12501bd059b36a94f47a07011a5a2d 100644 --- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb +++ b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb @@ -4,9 +4,7 @@ module Gitlab::Markdown describe TableOfContentsFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper def header(level, text) "<h#{level}>#{text}</h#{level}>\n" diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb index 2a1e1cc5127ddf15de70b9ee2d614d2bd7914060..94f39cc966ed55ed6903c85fd01842a19d08acae 100644 --- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb +++ b/spec/lib/gitlab/markdown/task_list_filter_spec.rb @@ -2,9 +2,7 @@ module Gitlab::Markdown describe TaskListFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper it 'does not apply `task-list` class to non-task lists' do exp = act = %(<ul><li>Item</li></ul>) diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index 0ecbdee9b9eb8074053a313afcc5000cdeb3a579..08e6941028f5db93ab3fa5c9294bf0ba3f167961 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -2,7 +2,7 @@ module Gitlab::Markdown describe UserReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/filter_spec_helper.rb similarity index 75% rename from spec/support/reference_filter_spec_helper.rb rename to spec/support/filter_spec_helper.rb index afbea55ab99ef2a726814f41416f7334813320e6..755964e9a3d3819416b52715ac508c1bf41775b1 100644 --- a/spec/support/reference_filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -1,46 +1,23 @@ -# Common methods and setup for Gitlab::Markdown reference filter specs +# Helper methods for Gitlab::Markdown filter specs # # Must be included into specs manually -module ReferenceFilterSpecHelper +module FilterSpecHelper extend ActiveSupport::Concern - # Shortcut to Rails' auto-generated routes helpers, to avoid including the - # module - def urls - Rails.application.routes.url_helpers - end - - # Modify a String reference to make it invalid - # - # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get - # their word characters reversed. - # - # reference - String reference to modify - # - # Returns a String - def invalidate_reference(reference) - if reference =~ /\A(.+)?.\d+\z/ - # Integer-based reference with optional project prefix - reference.gsub(/\d+\z/) { |i| i.to_i + 1 } - elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ - # SHA-based reference with optional prefix - reference.gsub(/\h{6,40}\z/) { |v| v.reverse } - else - reference.gsub(/\w+\z/) { |v| v.reverse } - end - end - # Perform `call` on the described class # - # Automatically passes the current `project` value to the context if none is - # provided. + # Automatically passes the current `project` value, if defined, to the context + # if none is provided. # - # html - String text to pass to the filter's `call` method. + # html - HTML String to pass to the filter's `call` method. # contexts - Hash context for the filter. (default: {project: project}) # - # Returns the String text returned by the filter's `call` method. + # Returns a Nokogiri::XML::DocumentFragment def filter(html, contexts = {}) - contexts.reverse_merge!(project: project) + if defined?(project) + contexts.reverse_merge!(project: project) + end + described_class.call(html, contexts) end @@ -50,7 +27,7 @@ def filter(html, contexts = {}) # body - String text to run through the pipeline # contexts - Hash context for the filter. (default: {project: project}) # - # Returns the Hash of the pipeline result + # Returns the Hash def pipeline_result(body, contexts = {}) contexts.reverse_merge!(project: project) @@ -58,13 +35,43 @@ def pipeline_result(body, contexts = {}) pipeline.call(body) end + # Modify a String reference to make it invalid + # + # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get + # their word characters reversed. + # + # reference - String reference to modify + # + # Returns a String + def invalidate_reference(reference) + if reference =~ /\A(.+)?.\d+\z/ + # Integer-based reference with optional project prefix + reference.gsub(/\d+\z/) { |i| i.to_i + 1 } + elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + # SHA-based reference with optional prefix + reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + else + reference.gsub(/\w+\z/) { |v| v.reverse } + end + end + + # Stub CrossProjectReference#user_can_reference_project? to return true for + # the current test def allow_cross_reference! allow_any_instance_of(described_class). to receive(:user_can_reference_project?).and_return(true) end + # Stub CrossProjectReference#user_can_reference_project? to return false for + # the current test def disallow_cross_reference! allow_any_instance_of(described_class). to receive(:user_can_reference_project?).and_return(false) end + + # Shortcut to Rails' auto-generated routes helpers, to avoid including the + # module + def urls + Rails.application.routes.url_helpers + end end