From 117d4e5c693f222c779a75bd29baca11de684780 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 15:10:59 -0500 Subject: [PATCH 01/18] Extract generic parts of Gitlab::Diff::InlineDiffMarker --- lib/gitlab/diff/inline_diff_marker.rb | 114 ++------------------ lib/gitlab/string_range_marker.rb | 102 ++++++++++++++++++ spec/lib/gitlab/string_range_marker_spec.rb | 36 +++++++ 3 files changed, 146 insertions(+), 106 deletions(-) create mode 100644 lib/gitlab/string_range_marker.rb create mode 100644 spec/lib/gitlab/string_range_marker_spec.rb diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 736933b1c4b..96f5ca31e5c 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -1,38 +1,18 @@ module Gitlab module Diff - class InlineDiffMarker + class InlineDiffMarker < Gitlab::StringRangeMarker MARKDOWN_SYMBOLS = { addition: "+", deletion: "-" }.freeze - attr_accessor :raw_line, :rich_line - - def initialize(raw_line, rich_line = raw_line) - @raw_line = raw_line - @rich_line = ERB::Util.html_escape(rich_line) - end - def mark(line_inline_diffs, mode: nil, markdown: false) - return rich_line unless line_inline_diffs - - marker_ranges = [] - line_inline_diffs.each do |inline_diff_range| - # Map the inline-diff range based on the raw line to character positions in the rich line - inline_diff_positions = position_mapping[inline_diff_range].flatten - # Turn the array of character positions into ranges - marker_ranges.concat(collapse_ranges(inline_diff_positions)) - end - - offset = 0 - - # Mark each range - marker_ranges.each_with_index do |range, index| + super(line_inline_diffs) do |text, left:, right:| before_content = if markdown "{#{MARKDOWN_SYMBOLS[mode]}" else - "<span class='#{html_class_names(marker_ranges, mode, index)}'>" + "<span class='#{html_class_names(left, right, mode)}'>" end after_content = if markdown @@ -40,98 +20,20 @@ module Gitlab else "</span>" end - offset = insert_around_range(rich_line, range, before_content, after_content, offset) - end - rich_line.html_safe + "#{before_content}#{text}#{after_content}" + end end private - def html_class_names(marker_ranges, mode, index) + def html_class_names(left, right, mode) class_names = ["idiff"] - class_names << "left" if index == 0 - class_names << "right" if index == marker_ranges.length - 1 + class_names << "left" if left + class_names << "right" if right class_names << mode if mode class_names.join(" ") end - - # Mapping of character positions in the raw line, to the rich (highlighted) line - def position_mapping - @position_mapping ||= begin - mapping = [] - rich_pos = 0 - (0..raw_line.length).each do |raw_pos| - rich_char = rich_line[rich_pos] - - # The raw and rich lines are the same except for HTML tags, - # so skip over any `<...>` segment - while rich_char == '<' - until rich_char == '>' - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - # multi-char HTML entities in the rich line correspond to a single character in the raw line - if rich_char == '&' - multichar_mapping = [rich_pos] - until rich_char == ';' - rich_pos += 1 - multichar_mapping << rich_pos - rich_char = rich_line[rich_pos] - end - - mapping[raw_pos] = multichar_mapping - else - mapping[raw_pos] = rich_pos - end - - rich_pos += 1 - end - - mapping - end - end - - # Takes an array of integers, and returns an array of ranges covering the same integers - def collapse_ranges(positions) - return [] if positions.empty? - ranges = [] - - start = prev = positions[0] - range = start..prev - positions[1..-1].each do |pos| - if pos == prev + 1 - range = start..pos - prev = pos - else - ranges << range - start = prev = pos - range = start..prev - end - end - ranges << range - - ranges - end - - # Inserts tags around the characters identified by the given range - def insert_around_range(text, range, before, after, offset = 0) - # Just to be sure - return offset if offset + range.end + 1 > text.length - - text.insert(offset + range.begin, before) - offset += before.length - - text.insert(offset + range.end + 1, after) - offset += after.length - - offset - end end end end diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb new file mode 100644 index 00000000000..94fba0a221a --- /dev/null +++ b/lib/gitlab/string_range_marker.rb @@ -0,0 +1,102 @@ +module Gitlab + class StringRangeMarker + attr_accessor :raw_line, :rich_line + + def initialize(raw_line, rich_line = raw_line) + @raw_line = raw_line + @rich_line = ERB::Util.html_escape(rich_line) + end + + def mark(marker_ranges) + return rich_line unless marker_ranges + + rich_marker_ranges = [] + marker_ranges.each do |range| + # Map the inline-diff range based on the raw line to character positions in the rich line + rich_positions = position_mapping[range].flatten + # Turn the array of character positions into ranges + rich_marker_ranges.concat(collapse_ranges(rich_positions)) + end + + offset = 0 + # Mark each range + rich_marker_ranges.each_with_index do |range, i| + offset_range = (range.begin + offset)..(range.end + offset) + original_text = rich_line[offset_range] + + text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1) + + rich_line[offset_range] = text + + offset += text.length - original_text.length + end + + rich_line.html_safe + end + + private + + # Mapping of character positions in the raw line, to the rich (highlighted) line + def position_mapping + @position_mapping ||= begin + mapping = [] + rich_pos = 0 + (0..raw_line.length).each do |raw_pos| + rich_char = rich_line[rich_pos] + + # The raw and rich lines are the same except for HTML tags, + # so skip over any `<...>` segment + while rich_char == '<' + until rich_char == '>' + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + # multi-char HTML entities in the rich line correspond to a single character in the raw line + if rich_char == '&' + multichar_mapping = [rich_pos] + until rich_char == ';' + rich_pos += 1 + multichar_mapping << rich_pos + rich_char = rich_line[rich_pos] + end + + mapping[raw_pos] = multichar_mapping + else + mapping[raw_pos] = rich_pos + end + + rich_pos += 1 + end + + mapping + end + end + + # Takes an array of integers, and returns an array of ranges covering the same integers + def collapse_ranges(positions) + return [] if positions.empty? + ranges = [] + + start = prev = positions[0] + range = start..prev + positions[1..-1].each do |pos| + if pos == prev + 1 + range = start..pos + prev = pos + else + ranges << range + start = prev = pos + range = start..prev + end + end + ranges << range + + ranges + end + end +end diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb new file mode 100644 index 00000000000..45b4d4af206 --- /dev/null +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::StringRangeMarker, lib: true do + describe '#mark' do + context "when the rich text is html safe" do + let(:raw) { "abc <def>" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } + let(:inline_diffs) { [2..5] } + let(:subject) do + described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT<dRIGHTef></span>}) + expect(subject).to be_html_safe + end + end + + context "when the rich text is not html safe" do + let(:raw) { "abc <def>" } + let(:inline_diffs) { [2..5] } + let(:subject) do + described_class.new(raw).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{abLEFTc <dRIGHTef>}) + expect(subject).to be_html_safe + end + end + end +end -- GitLab From 1875fa71ca0eba9aba0bdde3bf004e310705b6e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 15:17:20 -0500 Subject: [PATCH 02/18] Add Gitlab::StringRegexMarker --- lib/gitlab/string_regex_marker.rb | 13 +++++++++++++ spec/lib/gitlab/string_range_marker_spec.rb | 4 ++-- spec/lib/gitlab/string_regex_marker_spec.rb | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/string_regex_marker.rb create mode 100644 spec/lib/gitlab/string_regex_marker_spec.rb diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb new file mode 100644 index 00000000000..7ebf1c0428c --- /dev/null +++ b/lib/gitlab/string_regex_marker.rb @@ -0,0 +1,13 @@ +module Gitlab + class StringRegexMarker < StringRangeMarker + def mark(regex, group: 0, &block) + regex_match = raw_line.match(regex) + return rich_line unless regex_match + + begin_index, end_index = regex_match.offset(group) + name_range = begin_index..(end_index - 1) + + super([name_range], &block) + end + end +end diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb index 45b4d4af206..7c77772b3f6 100644 --- a/spec/lib/gitlab/string_range_marker_spec.rb +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::StringRangeMarker, lib: true do let(:raw) { "abc <def>" } let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } let(:inline_diffs) { [2..5] } - let(:subject) do + subject do described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:| "LEFT#{text}RIGHT" end @@ -21,7 +21,7 @@ describe Gitlab::StringRangeMarker, lib: true do context "when the rich text is not html safe" do let(:raw) { "abc <def>" } let(:inline_diffs) { [2..5] } - let(:subject) do + subject do described_class.new(raw).mark(inline_diffs) do |text, left:, right:| "LEFT#{text}RIGHT" end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb new file mode 100644 index 00000000000..2f5cf6c6e3b --- /dev/null +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::StringRegexMarker, lib: true do + describe '#mark' do + let(:raw) { %{"name": "AFNetworking"} } + let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } + subject do + described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| + %{<a href="#">#{text}</a>} + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) + expect(subject).to be_html_safe + end + end +end -- GitLab From 05d6abbdaf41840375705dd1647c8f64eb59f465 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 16:13:06 -0500 Subject: [PATCH 03/18] Autolink package names in Gemfile --- lib/gitlab/dependency_linker.rb | 20 ++++ lib/gitlab/dependency_linker/base_linker.rb | 96 +++++++++++++++++++ .../dependency_linker/gemfile_linker.rb | 25 +++++ lib/gitlab/highlight.rb | 37 +++++-- .../dependency_linker/gemfile_linker_spec.rb | 59 ++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 13 +++ spec/lib/gitlab/highlight_spec.rb | 11 +++ 7 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 lib/gitlab/dependency_linker.rb create mode 100644 lib/gitlab/dependency_linker/base_linker.rb create mode 100644 lib/gitlab/dependency_linker/gemfile_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb new file mode 100644 index 00000000000..656023afb8e --- /dev/null +++ b/lib/gitlab/dependency_linker.rb @@ -0,0 +1,20 @@ +module Gitlab + module DependencyLinker + LINKERS = [ + GemfileLinker, + ] + + def self.link(blob_name, plain_text, highlighted_text) + linker = linker(blob_name) + return highlighted_text unless linker + + linker.link(plain_text, highlighted_text) + end + + private + + def self.linker(blob_name) + LINKERS.find { |linker| linker.support?(blob_name) } + end + end +end diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb new file mode 100644 index 00000000000..defa5681c17 --- /dev/null +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -0,0 +1,96 @@ +module Gitlab + module DependencyLinker + class BaseLinker + def self.link(plain_text, highlighted_text) + new(plain_text, highlighted_text).link + end + + attr_accessor :plain_text, :highlighted_text + + def initialize(plain_text, highlighted_text) + @plain_text = plain_text + @highlighted_text = highlighted_text + end + + def link + link_dependencies + + highlighted_lines.join.html_safe + end + + private + + def package_url(name) + raise NotImplementedError + end + + def link_dependencies + raise NotImplementedError + end + + def package_link(name, url = package_url(name)) + return name unless url + + %{<a href="#{ERB::Util.html_escape_once(url)}">#{ERB::Util.html_escape_once(name)}</a>} + end + + # Links package names in a method call or assignment string argument. + # + # Example: + # link_method_call("gem") + # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"` + # + # link_method_call("gem", "specific_package") + # # Will link `specific_package` in `gem "specific_package"` + # + # link_method_call("github", /[^\/]+\/[^\/]+/) + # # Will link `user/repo` in `github "user/repo"`, but not `github "package"` + # + # link_method_call(%w[add_dependency add_development_dependency]) + # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"` + # + # link_method_call("name") + # # Will link `package` in `self.name = "package"` + def link_method_call(method_names, value = nil, &url_proc) + value = + case value + when String + Regexp.escape(value) + when nil + %{[^'"]+} + else + value + end + + method_names = Array(method_names).map { |name| Regexp.escape(name) } + link_regex(/#{Regexp.union(method_names)}\s*[(=]?\s*['"](?<name>#{value})['"]/, &url_proc) + end + + # Links package names based on regex. + # + # Example: + # link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/) + # # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"` + def link_regex(regex) + highlighted_lines.map!.with_index do |rich_line, i| + marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe) + + marked_line = marker.mark(regex, group: :name) do |text, left:, right:| + url = block_given? ? yield(text) : package_url(text) + package_link(text, url) + end + + marked_line + end + end + + def plain_lines + @plain_lines ||= plain_text.lines + end + + def highlighted_lines + @highlighted_lines ||= highlighted_text.lines + end + end + end +end diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb new file mode 100644 index 00000000000..1e0d08f2291 --- /dev/null +++ b/lib/gitlab/dependency_linker/gemfile_linker.rb @@ -0,0 +1,25 @@ +module Gitlab + module DependencyLinker + class GemfileLinker < BaseLinker + def self.support?(blob_name) + blob_name == 'Gemfile' || blob_name == 'gems.rb' + end + + private + + def link_dependencies + # Link `gem "package_name"` to https://rubygems.org/gems/package_name + link_method_call("gem") + + # Link `github: "user/repo"` to https://github.com/user/repo + link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/) do |name| + "https://github.com/#{name}" + end + end + + def package_url(name) + "https://rubygems.org/gems/#{name}" + end + end + end +end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index d787d5db4a0..83bc230df3e 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -13,6 +13,8 @@ module Gitlab highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe) end + attr_reader :blob_name + def initialize(blob_name, blob_content, repository: nil) @formatter = Rouge::Formatters::HTMLGitlab @repository = repository @@ -21,16 +23,9 @@ module Gitlab end def highlight(text, continue: true, plain: false) - if plain - hl_lexer = Rouge::Lexers::PlainText - continue = false - else - hl_lexer = self.lexer - end - - @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe - rescue - @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe + highlighted_text = highlight_text(text, continue: continue, plain: plain) + highlighted_text = link_dependencies(text, highlighted_text) if blob_name + highlighted_text end def lexer @@ -50,5 +45,27 @@ module Gitlab Rouge::Lexer.find_fancy(language_name) end + + def highlight_text(text, continue: true, plain: false) + if plain + highlight_plain(text) + else + highlight_rich(text, continue: continue) + end + end + + def highlight_plain(text) + @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe + end + + def highlight_rich(text, continue: true) + @formatter.format(lexer.lex(text, continue: continue), tag: lexer.tag).html_safe + rescue + highlight_plain(text) + end + + def link_dependencies(text, highlighted_text) + Gitlab::DependencyLinker.link(blob_name, text, highlighted_text) + end end end diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb new file mode 100644 index 00000000000..8f9d995fef6 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GemfileLinker, lib: true do + describe '.support?' do + it 'supports Gemfile' do + expect(described_class.support?('Gemfile')).to be_truthy + end + + it 'supports gems.rb' do + expect(described_class.support?('gems.rb')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Gemfile.lock')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Gemfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://rubygems.org' + + gem "rails", '4.2.6', github: "rails/rails" + gem 'rails-deprecated_sanitizer', '~> 1.0.3' + + # Responders respond_to and respond_with + gem 'responders', '~> 2.0', :github => 'rails/responders' + + # Specify a sprockets version due to increased performance + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 + gem 'sprockets', '~> 3.6.0' + + # Default values for AR models + gem 'default_value_for', '~> 3.0.0' + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("rails", "https://rubygems.org/gems/rails")) + expect(subject).to include(link("rails-deprecated_sanitizer", "https://rubygems.org/gems/rails-deprecated_sanitizer")) + expect(subject).to include(link("responders", "https://rubygems.org/gems/responders")) + expect(subject).to include(link("sprockets", "https://rubygems.org/gems/sprockets")) + expect(subject).to include(link("default_value_for", "https://rubygems.org/gems/default_value_for")) + end + + it "links GitHub repos" do + expect(subject).to include(link("rails/rails", "https://github.com/rails/rails")) + expect(subject).to include(link("rails/responders", "https://github.com/rails/responders")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb new file mode 100644 index 00000000000..03d5b61d70c --- /dev/null +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker, lib: true do + describe '.link' do + it 'links using GemfileLinker' do + blob_name = 'Gemfile' + + expect(described_class::GemfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + end +end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e49799ad105..e57b3053871 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -57,4 +57,15 @@ describe Gitlab::Highlight, lib: true do end end end + + describe '#highlight' do + subject { described_class.highlight(file_name, file_content, nowrap: false) } + + it 'links dependencies via DependencyLinker' do + expect(Gitlab::DependencyLinker).to receive(:link). + with('file.name', 'Contents', anything).and_call_original + + described_class.highlight('file.name', 'Contents') + end + end end -- GitLab From 9ec8d1b3eb5a2be8135764f5d4cce8e8597ee6e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 17:40:33 -0500 Subject: [PATCH 04/18] Autolink package names in gemspec --- lib/gitlab/dependency_linker.rb | 1 + lib/gitlab/dependency_linker/base_linker.rb | 4 ++ .../dependency_linker/gemspec_linker.rb | 16 +++++ .../dependency_linker/gemspec_linker_spec.rb | 63 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 5 files changed, 92 insertions(+) create mode 100644 lib/gitlab/dependency_linker/gemspec_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 656023afb8e..4e6e2f9df96 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -2,6 +2,7 @@ module Gitlab module DependencyLinker LINKERS = [ GemfileLinker, + GemspecLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index defa5681c17..027eb88e259 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -28,6 +28,10 @@ module Gitlab raise NotImplementedError end + def license_url(name) + "http://spdx.org/licenses/#{name}.html" if name =~ /[A-Za-z0-9.-]+/ + end + def package_link(name, url = package_url(name)) return name unless url diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb new file mode 100644 index 00000000000..cf8acc5b004 --- /dev/null +++ b/lib/gitlab/dependency_linker/gemspec_linker.rb @@ -0,0 +1,16 @@ +module Gitlab + module DependencyLinker + class GemspecLinker < GemfileLinker + def self.support?(blob_name) + blob_name.end_with?('.gemspec') + end + + private + + def link_dependencies + link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) + link_method_call("license", &method(:license_url)) + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb new file mode 100644 index 00000000000..a60fda533d3 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GemspecLinker, lib: true do + describe '.support?' do + it 'supports *.gemspec' do + expect(described_class.support?('gitlab_git.gemspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.gemspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "gitlab_git.gemspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Gem::Specification.new do |s| + s.name = 'gitlab_git' + s.version = `cat VERSION` + s.date = Time.now.strftime("%Y-%m-%d") + s.summary = "Gitlab::Git library" + s.description = "GitLab wrapper around git objects" + s.authors = ["Dmitriy Zaporozhets"] + s.email = 'dmitriy.zaporozhets@gmail.com' + s.license = 'MIT' + s.files = `git ls-files lib/`.split("\n") << 'VERSION' + s.homepage = + 'https://gitlab.com/gitlab-org/gitlab_git' + + s.add_dependency("github-linguist", "~> 4.7.0") + s.add_dependency("activesupport", "~> 4.0") + s.add_dependency("rugged", "~> 0.24.0") + s.add_runtime_dependency("charlock_holmes", "~> 0.7.3") + s.add_development_dependency("listen", "~> 3.0.6") + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("gitlab_git", "https://rubygems.org/gems/gitlab_git")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("github-linguist", "https://rubygems.org/gems/github-linguist")) + expect(subject).to include(link("activesupport", "https://rubygems.org/gems/activesupport")) + expect(subject).to include(link("rugged", "https://rubygems.org/gems/rugged")) + expect(subject).to include(link("charlock_holmes", "https://rubygems.org/gems/charlock_holmes")) + expect(subject).to include(link("listen", "https://rubygems.org/gems/listen")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 03d5b61d70c..7ba987026a4 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -9,5 +9,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using GemspecLinker' do + blob_name = 'gitlab_git.gemspec' + + expect(described_class::GemspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From b9f21795bc69a9a4882c6a0668fbbb3957feec84 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 17:41:25 -0500 Subject: [PATCH 05/18] Autolink package names in package.json --- lib/gitlab/dependency_linker.rb | 1 + lib/gitlab/dependency_linker/base_linker.rb | 44 ++++++++++++ lib/gitlab/dependency_linker/json_linker.rb | 17 +++++ .../dependency_linker/package_json_linker.rb | 35 ++++++++++ .../package_json_linker_spec.rb | 70 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 6 files changed, 175 insertions(+) create mode 100644 lib/gitlab/dependency_linker/json_linker.rb create mode 100644 lib/gitlab/dependency_linker/package_json_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 4e6e2f9df96..deb2950eea2 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -3,6 +3,7 @@ module Gitlab LINKERS = [ GemfileLinker, GemspecLinker, + PackageJsonLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 027eb88e259..bb589041d4d 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -70,6 +70,50 @@ module Gitlab link_regex(/#{Regexp.union(method_names)}\s*[(=]?\s*['"](?<name>#{value})['"]/, &url_proc) end + # Links package names in a JSON key or values. + # + # Example: + # link_json("name") + # # Will link `package` in `"name": "package"` + # + # link_json("name", "specific_package") + # # Will link `specific_package` in `"name": "specific_package"` + # + # link_json("name", /[^\/]+\/[^\/]+/) + # # Will link `user/repo` in `"name": "user/repo"`, but not `"name": "package"` + # + # link_json("specific_package", "1.0.1", package: :key) + # # Will link `specific_package` in `"specific_package": "1.0.1"` + def link_json(key, value = nil, package: :value, &url_proc) + key = + case key + when String + Regexp.escape(key) + when nil + '[^"]+' + else + key + end + + value = + case value + when String + Regexp.escape(value) + when nil + '[^"]+' + else + value + end + + if package == :value + value = "(?<name>#{value})" + else + key = "(?<name>#{key})" + end + + link_regex(/"#{key}":\s*"#{value}"/, &url_proc) + end + # Links package names based on regex. # # Example: diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb new file mode 100644 index 00000000000..4225cfe4126 --- /dev/null +++ b/lib/gitlab/dependency_linker/json_linker.rb @@ -0,0 +1,17 @@ +module Gitlab + module DependencyLinker + class JsonLinker < BaseLinker + def link + return highlighted_text unless json + + super + end + + private + + def json + @json ||= JSON.parse(plain_text) rescue nil + end + end + end +end diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb new file mode 100644 index 00000000000..7c5f3b97d88 --- /dev/null +++ b/lib/gitlab/dependency_linker/package_json_linker.rb @@ -0,0 +1,35 @@ +module Gitlab + module DependencyLinker + class PackageJsonLinker < JsonLinker + def self.support?(blob_name) + blob_name == 'package.json' + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("dependencies") + link_dependencies_at_key("devDependencies") + end + + def link_dependencies_at_key(key) + dependencies = json[key] + return unless dependencies + + dependencies.each do |name, version| + link_json(name, version, package: :key) + link_json(name, /[^\/"]+\/[^\/"]+/) do |name| + "https://github.com/#{name}" + end + end + end + + def package_url(name) + "https://npmjs.com/package/#{name}" + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb new file mode 100644 index 00000000000..49d917ff80b --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do + describe '.support?' do + it 'supports package.json' do + expect(described_class.support?('package.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('package.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "package.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "module-name", + "version": "10.3.1", + "dependencies": { + "primus": "*", + "async": "~0.8.0", + "express": "4.2.x", + "winston": "git://github.com/flatiron/winston#master", + "bigpipe": "bigpipe/pagelet", + "plates": "https://github.com/flatiron/plates/tarball/master" + }, + "devDependencies": { + "vows": "^0.7.0", + "assume": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", + "pre-commit": "*" + }, + "license": "MIT" + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the module name" do + expect(subject).to include(link("module-name", "https://npmjs.com/package/module-name")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("primus", "https://npmjs.com/package/primus")) + expect(subject).to include(link("async", "https://npmjs.com/package/async")) + expect(subject).to include(link("express", "https://npmjs.com/package/express")) + expect(subject).to include(link("winston", "https://npmjs.com/package/winston")) + expect(subject).to include(link("bigpipe", "https://npmjs.com/package/bigpipe")) + expect(subject).to include(link("plates", "https://npmjs.com/package/plates")) + expect(subject).to include(link("vows", "https://npmjs.com/package/vows")) + expect(subject).to include(link("assume", "https://npmjs.com/package/assume")) + expect(subject).to include(link("pre-commit", "https://npmjs.com/package/pre-commit")) + end + + it "links GitHub repos" do + expect(subject).to include(link("bigpipe/pagelet", "https://github.com/bigpipe/pagelet")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 7ba987026a4..53960b0a662 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -17,5 +17,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using PackageJsonLinker' do + blob_name = 'package.json' + + expect(described_class::PackageJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 502ef3cad4e84e5ef345b8433fc718007bbca450 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 17:45:31 -0500 Subject: [PATCH 06/18] Autolink package names in composer.json --- lib/gitlab/dependency_linker.rb | 1 + .../dependency_linker/composer_json_linker.rb | 23 +++++++ .../composer_json_linker_spec.rb | 67 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 4 files changed, 99 insertions(+) create mode 100644 lib/gitlab/dependency_linker/composer_json_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index deb2950eea2..75a0e508515 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -4,6 +4,7 @@ module Gitlab GemfileLinker, GemspecLinker, PackageJsonLinker, + ComposerJsonLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb new file mode 100644 index 00000000000..4c8a80a29ca --- /dev/null +++ b/lib/gitlab/dependency_linker/composer_json_linker.rb @@ -0,0 +1,23 @@ +module Gitlab + module DependencyLinker + class ComposerJsonLinker < PackageJsonLinker + def self.support?(blob_name) + blob_name == 'composer.json' + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("require") + link_dependencies_at_key("require-dev") + end + + def package_url(name) + "https://packagist.org/packages/#{name}" if name =~ /\A[^\/]+\/[^\/]+\z/ + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb new file mode 100644 index 00000000000..92bdf0fb370 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::ComposerJsonLinker, lib: true do + describe '.support?' do + it 'supports composer.json' do + expect(described_class.support?('composer.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('composer.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "composer.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "laravel/laravel", + "description": "The Laravel Framework.", + "keywords": ["framework", "laravel"], + "license": "MIT", + "type": "project", + "require": { + "php": ">=5.5.9", + "laravel/framework": "5.2.*" + }, + "require-dev": { + "fzaninotto/faker": "~1.4", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~4.0", + "symfony/css-selector": "2.8.*|3.0.*", + "symfony/dom-crawler": "2.8.*|3.0.*" + } + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the module name" do + expect(subject).to include(link("laravel/laravel", "https://packagist.org/packages/laravel/laravel")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("laravel/framework", "https://packagist.org/packages/laravel/framework")) + expect(subject).to include(link("fzaninotto/faker", "https://packagist.org/packages/fzaninotto/faker")) + expect(subject).to include(link("mockery/mockery", "https://packagist.org/packages/mockery/mockery")) + expect(subject).to include(link("phpunit/phpunit", "https://packagist.org/packages/phpunit/phpunit")) + expect(subject).to include(link("symfony/css-selector", "https://packagist.org/packages/symfony/css-selector")) + expect(subject).to include(link("symfony/dom-crawler", "https://packagist.org/packages/symfony/dom-crawler")) + end + + it "doesn't link core dependencies" do + expect(subject).not_to include(link("php", "https://packagist.org/packages/php")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 53960b0a662..7493e747e01 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -25,5 +25,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using ComposerJsonLinker' do + blob_name = 'composer.json' + + expect(described_class::ComposerJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 40f0c0d24ab5b1a375526b2b066d0fc231f76c1c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 17:50:47 -0500 Subject: [PATCH 07/18] Autolink package names in Podfile --- lib/gitlab/dependency_linker.rb | 1 + .../dependency_linker/podfile_linker.rb | 20 +++++++++++ .../dependency_linker/podfile_linker_spec.rb | 36 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++++ 4 files changed, 65 insertions(+) create mode 100644 lib/gitlab/dependency_linker/podfile_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 75a0e508515..e6e6184d428 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -5,6 +5,7 @@ module Gitlab GemspecLinker, PackageJsonLinker, ComposerJsonLinker, + PodfileLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb new file mode 100644 index 00000000000..6716d5bdfd4 --- /dev/null +++ b/lib/gitlab/dependency_linker/podfile_linker.rb @@ -0,0 +1,20 @@ +module Gitlab + module DependencyLinker + class PodfileLinker < BaseLinker + def self.support?(blob_name) + blob_name == 'Podfile' + end + + private + + def link_dependencies + link_method_call("pod") + end + + def package_url(name) + package = name.split("/", 2).first + "https://cocoapods.org/pods/#{package}" + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb new file mode 100644 index 00000000000..049306f9bd9 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodfileLinker, lib: true do + describe '.support?' do + it 'supports Podfile' do + expect(described_class.support?('Podfile')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Podfile.lock')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Podfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + target 'MyApp' + pod 'AFNetworking', '~> 1.0' + pod "RestKit/CoreData" + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("RestKit/CoreData", "https://cocoapods.org/pods/RestKit")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 7493e747e01..104e3a32e6c 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -33,5 +33,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using PodfileLinker' do + blob_name = 'Podfile' + + expect(described_class::PodfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 1d138bd0fd09530842bb10ca5084e3c47ca0f732 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 17:54:52 -0500 Subject: [PATCH 08/18] Autolink package names in podspec --- lib/gitlab/dependency_linker.rb | 1 + .../dependency_linker/podspec_linker.rb | 33 ++++++++++ .../dependency_linker/podspec_linker_spec.rb | 61 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 4 files changed, 103 insertions(+) create mode 100644 lib/gitlab/dependency_linker/podspec_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index e6e6184d428..4152c230e60 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -6,6 +6,7 @@ module Gitlab PackageJsonLinker, ComposerJsonLinker, PodfileLinker, + PodspecLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb new file mode 100644 index 00000000000..fd4b17e39bb --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_linker.rb @@ -0,0 +1,33 @@ +module Gitlab + module DependencyLinker + class PodspecLinker < PodfileLinker + def self.support?(blob_name) + blob_name.end_with?('.podspec') + end + + private + + def link_dependencies + link_method_call(%w[name dependency]) + + license_regex = %r{ + license + \s* + = + \s* + (?: + # spec.license = 'MIT' + ['"](?<name>[^'"]+)['"] + | + # spec.license = { :type => 'MIT' } + \{\s*:type\s*=>\s*['"](?<name>[^'"]+)['"] + | + # spec.license = { type: 'MIT' } + \{\s*type:\s*['"](?<name>[^'"]+)['"] + ) + }x + link_regex(license_regex, &method(:license_url)) + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb new file mode 100644 index 00000000000..597a6393c18 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecLinker, lib: true do + describe '.support?' do + it 'supports *.podspec' do + expect(described_class.support?('Reachability.podspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Reachability.podspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Pod::Spec.new do |spec| + spec.name = 'Reachability' + spec.version = '3.1.0' + spec.license = { :type => 'BSD' } + spec.license = "MIT" + spec.license = { type: 'Apache-1.0' } + spec.homepage = 'https://github.com/tonymillion/Reachability' + spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' } + spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.' + spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' } + spec.source_files = 'Reachability.{h,m}' + spec.framework = 'SystemConfiguration' + + spec.dependency 'AFNetworking', '~> 1.0' + spec.dependency 'RestKit/CoreData', '~> 0.20.0' + spec.ios.dependency 'MBProgressHUD', '~> 0.5' + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("Reachability", "https://cocoapods.org/pods/Reachability")) + end + + it "links the license" do + expect(subject).to include(link("BSD", "http://spdx.org/licenses/BSD.html")) + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + expect(subject).to include(link("Apache-1.0", "http://spdx.org/licenses/Apache-1.0.html")) + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("RestKit/CoreData", "https://cocoapods.org/pods/RestKit")) + expect(subject).to include(link("MBProgressHUD", "https://cocoapods.org/pods/MBProgressHUD")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 104e3a32e6c..b6ec756adeb 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -41,5 +41,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using PodspecLinker' do + blob_name = 'Reachability.podspec' + + expect(described_class::PodspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 0e0d6c70b8b33e171dc2ec528a4b7c7fbec7a5e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 18:04:47 -0500 Subject: [PATCH 09/18] Autolink package names in podspec.json --- lib/gitlab/dependency_linker.rb | 1 + .../dependency_linker/podspec_json_linker.rb | 39 ++++++++ .../podspec_json_linker_spec.rb | 88 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 ++ 4 files changed, 136 insertions(+) create mode 100644 lib/gitlab/dependency_linker/podspec_json_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 4152c230e60..0868d6be552 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -7,6 +7,7 @@ module Gitlab ComposerJsonLinker, PodfileLinker, PodspecLinker, + PodspecJsonLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/podspec_json_linker.rb b/lib/gitlab/dependency_linker/podspec_json_linker.rb new file mode 100644 index 00000000000..825c548c171 --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_json_linker.rb @@ -0,0 +1,39 @@ +module Gitlab + module DependencyLinker + class PodspecJsonLinker < JsonLinker + def self.support?(blob_name) + blob_name.end_with?('.podspec.json') + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("dependencies") + + subspecs = json["subspecs"] + if subspecs + subspecs.each do |subspec| + link_dependencies_at_key("dependencies", subspec) + end + end + end + + def link_dependencies_at_key(key, root = json) + dependencies = root[key] + return unless dependencies + + dependencies.each do |name, _| + link_regex(/"(?<name>#{Regexp.escape(name)})":\s*\[/) + end + end + + def package_url(name) + package = name.split("/", 2).first + "https://cocoapods.org/pods/#{package}" + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb new file mode 100644 index 00000000000..a508def683f --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecJsonLinker, lib: true do + describe '.support?' do + it 'supports *.podspec.json' do + expect(described_class.support?('Reachability.podspec.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "AFNetworking.podspec.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "AFNetworking", + "version": "2.0.0", + "license": "MIT", + "summary": "A delightful iOS and OS X networking framework.", + "homepage": "https://github.com/AFNetworking/AFNetworking", + "authors": { + "Mattt Thompson": "m@mattt.me" + }, + "source": { + "git": "https://github.com/AFNetworking/AFNetworking.git", + "tag": "2.0.0", + "submodules": true + }, + "requires_arc": true, + "platforms": { + "ios": "6.0", + "osx": "10.8" + }, + "public_header_files": "AFNetworking/*.h", + "subspecs": [ + { + "name": "NSURLConnection", + "dependencies": { + "AFNetworking/Serialization": [ + + ], + "AFNetworking/Reachability": [ + + ], + "AFNetworking/Security": [ + + ] + }, + "source_files": [ + "AFNetworking/AFURLConnectionOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperationManager.{h,m}" + ] + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking/Serialization", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("AFNetworking/Reachability", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("AFNetworking/Security", "https://cocoapods.org/pods/AFNetworking")) + end + + it "doesn't link subspec names" do + expect(subject).not_to include(link("NSURLConnection", "https://cocoapods.org/pods/NSURLConnection")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index b6ec756adeb..9fc539b604a 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -49,5 +49,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using PodspecJsonLinker' do + blob_name = 'AFNetworking.podspec.json' + + expect(described_class::PodspecJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 2defe9ec45f5caa777086e13ef6bc2348dd328ae Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 18:24:08 -0500 Subject: [PATCH 10/18] Autolink package names in Cartfile --- lib/gitlab/dependency_linker.rb | 1 + lib/gitlab/dependency_linker/base_linker.rb | 2 +- .../dependency_linker/cartfile_linker.rb | 19 ++++++ .../dependency_linker/cartfile_linker_spec.rb | 62 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/dependency_linker/cartfile_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 0868d6be552..f45c22a682b 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -8,6 +8,7 @@ module Gitlab PodfileLinker, PodspecLinker, PodspecJsonLinker, + CartfileLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index bb589041d4d..d5449eafc40 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -47,7 +47,7 @@ module Gitlab # link_method_call("gem", "specific_package") # # Will link `specific_package` in `gem "specific_package"` # - # link_method_call("github", /[^\/]+\/[^\/]+/) + # link_method_call("github", /[^\/"]+\/[^\/"]+/) # # Will link `user/repo` in `github "user/repo"`, but not `github "package"` # # link_method_call(%w[add_dependency add_development_dependency]) diff --git a/lib/gitlab/dependency_linker/cartfile_linker.rb b/lib/gitlab/dependency_linker/cartfile_linker.rb new file mode 100644 index 00000000000..9f7d10b5974 --- /dev/null +++ b/lib/gitlab/dependency_linker/cartfile_linker.rb @@ -0,0 +1,19 @@ +module Gitlab + module DependencyLinker + class CartfileLinker < BaseLinker + def self.support?(blob_name) + blob_name.start_with?('Cartfile') + end + + private + + def link_dependencies + link_method_call("github", /[^\/"]+\/[^\/"]+/) + end + + def package_url(name) + "https://github.com/#{name}" + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb new file mode 100644 index 00000000000..d562f81fab8 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::CartfileLinker, lib: true do + describe '.support?' do + it 'supports Cartfile' do + expect(described_class.support?('Cartfile')).to be_truthy + end + + it 'supports Cartfile.private' do + expect(described_class.support?('Cartfile.private')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('test.Cartfile')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Cartfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # Require version 2.3.1 or later + github "ReactiveCocoa/ReactiveCocoa" >= 2.3.1 + + # Require version 1.x + github "Mantle/Mantle" ~> 1.0 # (1.0 or later, but less than 2.0) + + # Require exactly version 0.4.1 + github "jspahrsummers/libextobjc" == 0.4.1 + + # Use the latest version + github "jspahrsummers/xcconfigs" + + # Use the branch + github "jspahrsummers/xcconfigs" "branch" + + # Use a project from GitHub Enterprise + github "https://enterprise.local/ghe/desktop/git-error-translations" + + # Use a project from any arbitrary server, on the "development" branch + git "https://enterprise.local/desktop/git-error-translations2.git" "development" + + # Use a local project + git "file:///directory/to/project" "branch" + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("ReactiveCocoa/ReactiveCocoa", "https://github.com/ReactiveCocoa/ReactiveCocoa")) + expect(subject).to include(link("Mantle/Mantle", "https://github.com/Mantle/Mantle")) + expect(subject).to include(link("jspahrsummers/libextobjc", "https://github.com/jspahrsummers/libextobjc")) + expect(subject).to include(link("jspahrsummers/xcconfigs", "https://github.com/jspahrsummers/xcconfigs")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 9fc539b604a..f27f633a62c 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -57,5 +57,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using CartfileLinker' do + blob_name = 'Cartfile' + + expect(described_class::CartfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From bcbefb9d2936287cd3c1ea7f4a537ea529455fe2 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 18:30:30 -0500 Subject: [PATCH 11/18] Autolink package names in Godeps.json --- lib/gitlab/dependency_linker.rb | 1 + .../dependency_linker/godeps_json_linker.rb | 23 +++++++ .../godeps_json_linker_spec.rb | 67 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 +++ 4 files changed, 99 insertions(+) create mode 100644 lib/gitlab/dependency_linker/godeps_json_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index f45c22a682b..37923dcc017 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -9,6 +9,7 @@ module Gitlab PodspecLinker, PodspecJsonLinker, CartfileLinker, + GodepsJsonLinker, ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb new file mode 100644 index 00000000000..227f7439fa0 --- /dev/null +++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb @@ -0,0 +1,23 @@ +module Gitlab + module DependencyLinker + class GodepsJsonLinker < BaseLinker + def self.support?(blob_name) + blob_name == 'Godeps.json' + end + + private + + def link_dependencies + link_json("ImportPath") + end + + def package_url(name) + if name =~ /\A(?<repo>git(lab|hub)\.com\/[^\/]+\/[^\/]+)\/(?<path>.+)\z/ + "http://#{$~[:repo]}/tree/master/#{$~[:path]}" + else + "http://#{name}" + end + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb new file mode 100644 index 00000000000..a9ae721f05e --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GodepsJsonLinker, lib: true do + describe '.support?' do + it 'supports Godeps.json' do + expect(described_class.support?('Godeps.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Godeps.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Godeps.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "ImportPath": "gitlab.com/gitlab-org/gitlab-pages", + "GoVersion": "go1.5", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/kardianos/osext", + "Rev": "efacde03154693404c65e7aa7d461ac9014acd0c" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "github.com/stretchr/testify/require", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "golang.org/x/crypto/ssh/terminal", + "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d" + }, + { + "ImportPath": "golang.org/x/net/http2", + "Rev": "b4e17d61b15679caf2335da776c614169a1b4643" + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the package name" do + expect(subject).to include(link("gitlab.com/gitlab-org/gitlab-pages", "http://gitlab.com/gitlab-org/gitlab-pages")) + end + + it "links dependencies" do + expect(subject).to include(link("github.com/kardianos/osext", "http://github.com/kardianos/osext")) + expect(subject).to include(link("github.com/stretchr/testify/assert", "http://github.com/stretchr/testify/tree/master/assert")) + expect(subject).to include(link("github.com/stretchr/testify/require", "http://github.com/stretchr/testify/tree/master/require")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index f27f633a62c..15a8bf6a426 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -65,5 +65,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using GodepsJsonLinker' do + blob_name = 'Godeps.json' + + expect(described_class::GodepsJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 4b1b42800f71cde2224db1c00ef8fec53c6ff99b Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 18:58:55 -0500 Subject: [PATCH 12/18] Autolink package names in requirements.txt --- lib/gitlab/dependency_linker.rb | 1 + .../requirements_txt_linker.rb | 19 +++++ .../requirements_txt_linker_spec.rb | 83 +++++++++++++++++++ spec/lib/gitlab/dependency_linker_spec.rb | 8 ++ 4 files changed, 111 insertions(+) create mode 100644 lib/gitlab/dependency_linker/requirements_txt_linker.rb create mode 100644 spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index 37923dcc017..57fc3c6e0e5 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -10,6 +10,7 @@ module Gitlab PodspecJsonLinker, CartfileLinker, GodepsJsonLinker, + RequirementsTxtLinker ] def self.link(blob_name, plain_text, highlighted_text) diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb new file mode 100644 index 00000000000..92fcb8177ea --- /dev/null +++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb @@ -0,0 +1,19 @@ +module Gitlab + module DependencyLinker + class RequirementsTxtLinker < BaseLinker + def self.support?(blob_name) + blob_name.end_with?('requirements.txt') + end + + private + + def link_dependencies + link_regex(/(?<name>^(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) + end + + def package_url(name) + "https://pypi.python.org/pypi/#{name}" + end + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb new file mode 100644 index 00000000000..a334bb38db1 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do + describe '.support?' do + it 'supports requirements.txt' do + expect(described_class.support?('requirements.txt')).to be_truthy + end + + it 'supports doc-requirements.txt' do + expect(described_class.support?('doc-requirements.txt')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('requirements')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "requirements.txt" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # + ####### example-requirements.txt ####### + # + ###### Requirements without Version Specifiers ###### + nose + nose-cov + beautifulsoup4 + # + ###### Requirements with Version Specifiers ###### + # See https://www.python.org/dev/peps/pep-0440/#version-specifiers + docopt == 0.6.1 # Version Matching. Must be version 0.6.1 + keyring >= 4.1.1 # Minimum version 4.1.1 + coverage != 3.5 # Version Exclusion. Anything except version 3.5 + Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* + # + ###### Refer to other requirements files ###### + -r other-requirements.txt + # + # + ###### A particular file ###### + ./downloads/numpy-1.9.2-cp34-none-win32.whl + http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl + # + ###### Additional Requirements without Version Specifiers ###### + # Same as 1st section, just here to show that you can put things in any order. + rejected + green + # + + Jinja2>=2.3 + Pygments>=1.2 + Sphinx>=1.3 + docutils>=0.7 + markupsafe + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("nose", "https://pypi.python.org/pypi/nose")) + expect(subject).to include(link("nose-cov", "https://pypi.python.org/pypi/nose-cov")) + expect(subject).to include(link("beautifulsoup4", "https://pypi.python.org/pypi/beautifulsoup4")) + expect(subject).to include(link("docopt", "https://pypi.python.org/pypi/docopt")) + expect(subject).to include(link("keyring", "https://pypi.python.org/pypi/keyring")) + expect(subject).to include(link("coverage", "https://pypi.python.org/pypi/coverage")) + expect(subject).to include(link("Mopidy-Dirble", "https://pypi.python.org/pypi/Mopidy-Dirble")) + expect(subject).to include(link("rejected", "https://pypi.python.org/pypi/rejected")) + expect(subject).to include(link("green", "https://pypi.python.org/pypi/green")) + expect(subject).to include(link("Jinja2", "https://pypi.python.org/pypi/Jinja2")) + expect(subject).to include(link("Pygments", "https://pypi.python.org/pypi/Pygments")) + expect(subject).to include(link("Sphinx", "https://pypi.python.org/pypi/Sphinx")) + expect(subject).to include(link("docutils", "https://pypi.python.org/pypi/docutils")) + expect(subject).to include(link("markupsafe", "https://pypi.python.org/pypi/markupsafe")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 15a8bf6a426..3d1cfbcfbf7 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -73,5 +73,13 @@ describe Gitlab::DependencyLinker, lib: true do described_class.link(blob_name, nil, nil) end + + it 'links using RequirementsTxtLinker' do + blob_name = 'requirements.txt' + + expect(described_class::RequirementsTxtLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end -- GitLab From 26992add96ad3f5b0d5f8b8d1bf415fabfa3bba3 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 15:12:45 -0500 Subject: [PATCH 13/18] Make AutolinkFilter smarter about trailing punctuation --- lib/banzai/filter/autolink_filter.rb | 2 +- spec/lib/banzai/filter/autolink_filter_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index b8d2673c1a6..a8ca2757612 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -26,7 +26,7 @@ module Banzai # in the generated link. # # Rubular: http://rubular.com/r/cxjPyZc7Sb - LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?<!,|\.)} + LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?<![,."':?!&)<>])} # Text matching LINK_PATTERN inside these elements will not be linked IGNORE_PARENTS = %w(a code kbd pre script style).to_set diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index a6d2ea11fcc..7fdcd1148aa 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -130,6 +130,15 @@ describe Banzai::Filter::AutolinkFilter, lib: true do doc = filter("See #{link}...") expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}\"") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}'") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link})") + expect(doc.at_css('a').text).to eq link end it 'does not include trailing HTML entities' do -- GitLab From f5cf7cdb4d79b11a51e19fea2d73f9cb84d1bda2 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 15:38:46 -0500 Subject: [PATCH 14/18] Autolink URLs and emails in blobs and diffs --- app/assets/stylesheets/highlight/dark.scss | 5 ++ app/assets/stylesheets/highlight/monokai.scss | 5 ++ .../stylesheets/highlight/solarized_dark.scss | 5 ++ .../highlight/solarized_light.scss | 5 ++ app/assets/stylesheets/highlight/white.scss | 5 ++ lib/banzai/filter/autolink_filter.rb | 10 ++- lib/banzai/pipeline/autolink_pipeline.rb | 12 ++++ lib/gitlab/highlight.rb | 20 +++++- spec/lib/gitlab/highlight_spec.rb | 65 ++++++++++++++++++- 9 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 lib/banzai/pipeline/autolink_pipeline.rb diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 09951fe3d3e..6e3829d994f 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -185,6 +185,11 @@ $dark-il: #de935f; color: $dark-highlight-color !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $dark-na; + } + .hll { background-color: $dark-hll-bg; } .c { color: $dark-c; } /* Comment */ .err { color: $dark-err; } /* Error */ diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index b6a6d298adf..68eb0c7720f 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -185,6 +185,11 @@ $monokai-gi: #a6e22e; color: $black !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $monokai-k; + } + .hll { background-color: $monokai-hll; } .c { color: $monokai-c; } /* Comment */ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 4f7a50dcb4f..2cc968c32f2 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -188,6 +188,11 @@ $solarized-dark-il: #2aa198; background-color: $solarized-dark-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $solarized-dark-kd; + } + /* Solarized Dark For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 6463fe96c1b..b61b85a2cd1 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -196,6 +196,11 @@ $solarized-light-il: #2aa198; background-color: $solarized-light-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $solarized-light-kd; + } + /* Solarized Light For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index ab2018bfbca..1daa10aef24 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -203,6 +203,11 @@ $white-gc-bg: #eaf2f5; background-color: $white-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $white-nb; + } + .hll { background-color: $white-hll-bg; } .c { color: $white-c; font-style: italic; } .err { color: $white-err; background-color: $white-err-bg; } diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index a8ca2757612..15f7da5d934 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -54,15 +54,13 @@ module Banzai # # `@doc` will be re-parsed with the HTML String from Rinku. def rinku_parse - # Convert the options from a Hash to a String that Rinku expects - options = tag_options(link_options) - # NOTE: We don't parse email links because it will erroneously match # external Commit and CommitRange references. # # The final argument tells Rinku to link short URLs that don't include a # period (e.g., http://localhost:3000/) - rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1) + mode = context[:autolink_emails] ? :all : :urls + rinku = Rinku.auto_link(html, mode, tag_options(link_options), IGNORE_PARENTS.to_a, 1) return if rinku == html @@ -111,9 +109,9 @@ module Banzai # order to be output literally rather than escaped. match.gsub!(/((?:&[\w#]+;)+)\z/, '') dropped = ($1 || '').html_safe + match = ERB::Util.html_escape_once(match) - options = link_options.merge(href: match) - content_tag(:a, match, options) + dropped + %{<a href="#{match}" #{tag_options(link_options)}>#{match}</a>#{dropped}}.html_safe end def autolink_filter(text) diff --git a/lib/banzai/pipeline/autolink_pipeline.rb b/lib/banzai/pipeline/autolink_pipeline.rb new file mode 100644 index 00000000000..53f2da5c7b5 --- /dev/null +++ b/lib/banzai/pipeline/autolink_pipeline.rb @@ -0,0 +1,12 @@ +module Banzai + module Pipeline + class AutolinkPipeline < BasePipeline + def self.filters + @filters ||= FilterArray[ + Filter::AutolinkFilter, + Filter::ExternalLinkFilter + ] + end + end + end +end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 83bc230df3e..57ae127254b 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -25,7 +25,7 @@ module Gitlab def highlight(text, continue: true, plain: false) highlighted_text = highlight_text(text, continue: continue, plain: plain) highlighted_text = link_dependencies(text, highlighted_text) if blob_name - highlighted_text + autolink_strings(highlighted_text) end def lexer @@ -67,5 +67,23 @@ module Gitlab def link_dependencies(text, highlighted_text) Gitlab::DependencyLinker.link(blob_name, text, highlighted_text) end + + def autolink_strings(highlighted_text) + doc = Nokogiri::HTML::DocumentFragment.parse(highlighted_text) + + # Files without highlighting have all text in `span.line`. + # Files with highlighting have strings and comments in `span`s with a + # `class` starting with `c` or `s`. + doc.xpath('.//span[@class="line" or starts-with(@class, "c") or starts-with(@class, "s")]/text()').each do |node| + content = node.to_html + html = Banzai.render(content, pipeline: :autolink, autolink_emails: true) + + next if html == content + + node.replace(html) + end + + doc.to_html.html_safe + end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e57b3053871..4f6ae157d6e 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Highlight, lib: true do describe '.highlight_lines' do let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') + described_class.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end it 'highlights all the lines properly' do @@ -59,7 +59,68 @@ describe Gitlab::Highlight, lib: true do end describe '#highlight' do - subject { described_class.highlight(file_name, file_content, nowrap: false) } + subject { described_class.highlight(file_name, file_content) } + + context "plain text file" do + let(:file_name) { "example.txt" } + let(:file_content) do + <<-CONTENT.strip_heredoc + URL: http://www.google.com + Email: hello@example.com + CONTENT + end + + it "links URLs" do + expect(subject).to include(%{<a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{<a href="mailto:hello@example.com">hello@example.com</a>}) + end + end + + context "file with highlighting" do + let(:file_name) { "example.rb" } + let(:file_content) do + <<-CONTENT.strip_heredoc + # URL in comment: http://www.google.com + # Email in comment: hello@example.com + + "URL in string: http://www.google.com" + "Email in string: hello@example.com" + + # <http://www.google.com> + # <url>http://www.google.com</url> + CONTENT + end + + context "in a comment" do + it "links URLs" do + expect(subject).to include(%{URL in comment: <a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{Email in comment: <a href="mailto:hello@example.com">hello@example.com</a>}) + end + end + + context "in a string" do + it "links URLs" do + expect(subject).to include(%{URL in string: <a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{Email in string: <a href="mailto:hello@example.com">hello@example.com</a>}) + end + end + + context 'in HTML/XML tags' do + it "links URLs" do + expect(subject).to include(%{<<a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>>}) + expect(subject).to include(%{<url><a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a></url>}) + end + end + end it 'links dependencies via DependencyLinker' do expect(Gitlab::DependencyLinker).to receive(:link). -- GitLab From 820cfa14a5216b5bb27bb34ba2dec77867901aee Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Sun, 10 Jul 2016 15:39:20 -0500 Subject: [PATCH 15/18] Update specs --- .../lib/gitlab/diff/inline_diff_marker_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 198ff977f24..418521ae650 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -1,26 +1,26 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do - describe '#inline_diffs' do + describe '#mark' do context "when the rich text is html safe" do - let(:raw) { "abc 'def'" } - let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">'def'</span>}.html_safe } + let(:raw) { "abc <def>" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + let(:subject) { described_class.new(raw, rich).mark(inline_diffs) } it 'marks the inline diffs' do - expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>'d</span>ef'</span>}) + expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'><d</span>ef></span>}) expect(subject).to be_html_safe end end - context "when the text text is not html safe" do - let(:raw) { "abc 'def'" } + context "when the text is not html safe" do + let(:raw) { "abc <def>" } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + let(:subject) { described_class.new(raw).mark(inline_diffs) } it 'marks the inline diffs' do - expect(subject).to eq(%{ab<span class='idiff left right'>c 'd</span>ef'}) + expect(subject).to eq(%{ab<span class='idiff left right'>c <d</span>ef>}) expect(subject).to be_html_safe end end -- GitLab From bc6e3daa592a4a1b7de19bc55be2a4bea8008cf0 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Tue, 9 May 2017 15:59:21 -0500 Subject: [PATCH 16/18] Add autolink and link_deps flags to ease benchmarking --- lib/gitlab/highlight.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 57ae127254b..558a26810b7 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,8 +1,8 @@ module Gitlab class Highlight - def self.highlight(blob_name, blob_content, repository: nil, plain: false) + def self.highlight(blob_name, blob_content, repository: nil, plain: false, link_deps: true, autolink: true) new(blob_name, blob_content, repository: repository). - highlight(blob_content, continue: false, plain: plain) + highlight(blob_content, continue: false, plain: plain, link_deps: link_deps, autolink: autolink) end def self.highlight_lines(repository, ref, file_name) @@ -22,10 +22,11 @@ module Gitlab @blob_content = blob_content end - def highlight(text, continue: true, plain: false) + def highlight(text, continue: true, plain: false, link_deps: true, autolink: true) highlighted_text = highlight_text(text, continue: continue, plain: plain) - highlighted_text = link_dependencies(text, highlighted_text) if blob_name - autolink_strings(highlighted_text) + highlighted_text = link_dependencies(text, highlighted_text) if blob_name && link_deps + highlighted_text = autolink_strings(highlighted_text) if autolink + highlighted_text end def lexer -- GitLab From 180bb5fa08f0524855e919ddc3c3fbf38e88334c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Tue, 9 May 2017 16:00:17 -0500 Subject: [PATCH 17/18] Autolink all of the highlighted text at once --- lib/gitlab/highlight.rb | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 558a26810b7..9013655ff3a 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -70,21 +70,8 @@ module Gitlab end def autolink_strings(highlighted_text) - doc = Nokogiri::HTML::DocumentFragment.parse(highlighted_text) - - # Files without highlighting have all text in `span.line`. - # Files with highlighting have strings and comments in `span`s with a - # `class` starting with `c` or `s`. - doc.xpath('.//span[@class="line" or starts-with(@class, "c") or starts-with(@class, "s")]/text()').each do |node| - content = node.to_html - html = Banzai.render(content, pipeline: :autolink, autolink_emails: true) - - next if html == content - - node.replace(html) - end - - doc.to_html.html_safe + # TODO: Don't run pre-processing pipeline, because this may break the highlighting + Banzai.render(highlighted_text, pipeline: :autolink, autolink_emails: true).html_safe end end end -- GitLab From 968d68258afcc61dcab3882f7a32431e38204189 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Tue, 9 May 2017 16:02:55 -0500 Subject: [PATCH 18/18] =?UTF-8?q?Autolink=20the=20raw=20text=20and=20?= =?UTF-8?q?=E2=80=9Ctransfer=E2=80=9D=20the=20found=20links=20to=20the=20h?= =?UTF-8?q?ighlighted=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gitlab/highlight.rb | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 9013655ff3a..0d5c7a92540 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -25,7 +25,7 @@ module Gitlab def highlight(text, continue: true, plain: false, link_deps: true, autolink: true) highlighted_text = highlight_text(text, continue: continue, plain: plain) highlighted_text = link_dependencies(text, highlighted_text) if blob_name && link_deps - highlighted_text = autolink_strings(highlighted_text) if autolink + highlighted_text = autolink_strings(text, highlighted_text) if autolink highlighted_text end @@ -69,9 +69,42 @@ module Gitlab Gitlab::DependencyLinker.link(blob_name, text, highlighted_text) end - def autolink_strings(highlighted_text) + def autolink_strings(text, highlighted_text) + raw_lines = text.lines + # TODO: Don't run pre-processing pipeline, because this may break the highlighting - Banzai.render(highlighted_text, pipeline: :autolink, autolink_emails: true).html_safe + linked_text = Banzai.render( + ERB::Util.html_escape(text), + pipeline: :autolink, + autolink_emails: true + ).html_safe + + linked_lines = linked_text.lines + + highlighted_lines = highlighted_text.lines + + highlighted_lines.map!.with_index do |rich_line, i| + matches = [] + linked_lines[i].scan(/(?<start><a[^>]+>)(?<content>[^<]+)(?<end><\/a>)/) { matches << Regexp.last_match } + next rich_line if matches.empty? + + raw_line = raw_lines[i] + marked_line = rich_line.html_safe + + matches.each do |match| + marker = StringRegexMarker.new(raw_line, marked_line) + + regex = /#{Regexp.escape(match[:content])}/ + + marked_line = marker.mark(regex) do |text, left:, right:| + "#{match[:start]}#{text}#{match[:end]}" + end + end + + marked_line + end + + highlighted_lines.join.html_safe end end end -- GitLab