From 08706f683725dcfbda1708ddf079347d989fbef1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 8 May 2017 18:50:23 -0500 Subject: [PATCH] Implement auxiliary blob viewers --- app/assets/javascripts/blob/viewer/index.js | 99 ++++++++++++------- app/controllers/concerns/renders_blob.rb | 5 +- app/helpers/projects_helper.rb | 7 +- app/models/blob.rb | 34 ++++--- app/models/blob_viewer/auxiliary.rb | 12 +++ app/models/blob_viewer/base.rb | 30 ++++-- app/models/blob_viewer/gitlab_ci_yml.rb | 23 +++++ app/models/blob_viewer/license.rb | 23 +++++ app/models/blob_viewer/route_map.rb | 30 ++++++ app/models/blob_viewer/server_side.rb | 6 ++ app/models/repository.rb | 11 ++- app/views/projects/blob/_blob.html.haml | 5 + app/views/projects/blob/_viewer.html.haml | 3 +- .../blob/viewers/_gitlab_ci_yml.html.haml | 9 ++ .../viewers/_gitlab_ci_yml_loading.html.haml | 4 + .../projects/blob/viewers/_license.html.haml | 8 ++ .../projects/blob/viewers/_loading.html.haml | 2 + .../blob/viewers/_loading_auxiliary.html.haml | 2 + .../blob/viewers/_route_map.html.haml | 9 ++ .../blob/viewers/_route_map_loading.html.haml | 4 + doc/ci/environments.md | 3 +- lib/gitlab/file_detector.rb | 3 +- .../features/projects/blobs/blob_show_spec.rb | 98 ++++++++++++++---- spec/models/blob_spec.rb | 46 +++++++++ spec/models/blob_viewer/base_spec.rb | 72 +++++++------- spec/models/blob_viewer/gitlab_ci_yml_spec.rb | 32 ++++++ spec/models/blob_viewer/license_spec.rb | 34 +++++++ spec/models/blob_viewer/route_map_spec.rb | 38 +++++++ spec/models/blob_viewer/server_side_spec.rb | 25 +++++ spec/models/repository_spec.rb | 51 ++++++++-- .../projects/blob/_viewer.html.haml_spec.rb | 4 +- 31 files changed, 598 insertions(+), 134 deletions(-) create mode 100644 app/models/blob_viewer/auxiliary.rb create mode 100644 app/models/blob_viewer/gitlab_ci_yml.rb create mode 100644 app/models/blob_viewer/license.rb create mode 100644 app/models/blob_viewer/route_map.rb create mode 100644 app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml create mode 100644 app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml create mode 100644 app/views/projects/blob/viewers/_license.html.haml create mode 100644 app/views/projects/blob/viewers/_loading.html.haml create mode 100644 app/views/projects/blob/viewers/_loading_auxiliary.html.haml create mode 100644 app/views/projects/blob/viewers/_route_map.html.haml create mode 100644 app/views/projects/blob/viewers/_route_map_loading.html.haml create mode 100644 spec/models/blob_viewer/gitlab_ci_yml_spec.rb create mode 100644 spec/models/blob_viewer/license_spec.rb create mode 100644 spec/models/blob_viewer/route_map_spec.rb create mode 100644 spec/models/blob_viewer/server_side_spec.rb diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 69ff2f95799..849da633c89 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,20 +1,38 @@ /* global Flash */ export default class BlobViewer { constructor() { + BlobViewer.initAuxiliaryViewer(); + + this.initMainViewers(); + } + + static initAuxiliaryViewer() { + const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]'); + if (!auxiliaryViewer) return; + + BlobViewer.loadViewer(auxiliaryViewer); + } + + initMainViewers() { + this.$fileHolder = $('.file-holder'); + if (!this.$fileHolder.length) return; + this.switcher = document.querySelector('.js-blob-viewer-switcher'); this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn'); this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); - this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]'); - this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]'); - this.$fileHolder = $('.file-holder'); - const initialViewer = document.querySelector('.blob-viewer:not(.hidden)'); - if (!initialViewer) return; - - let initialViewerName = initialViewer.getAttribute('data-type'); + this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]'); + this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]'); this.initBindings(); + this.switchToInitialViewer(); + } + + switchToInitialViewer() { + const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)'); + let initialViewerName = initialViewer.getAttribute('data-type'); + if (this.switcher && location.hash.indexOf('#L') === 0) { initialViewerName = 'simple'; } @@ -64,40 +82,13 @@ export default class BlobViewer { $(this.copySourceBtn).tooltip('fixTitle'); } - loadViewer(viewerParam) { - const viewer = viewerParam; - const url = viewer.getAttribute('data-url'); - - if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { - return; - } - - viewer.setAttribute('data-loading', 'true'); - - $.ajax({ - url, - dataType: 'JSON', - }) - .fail(() => new Flash('Error loading source view')) - .done((data) => { - viewer.innerHTML = data.html; - $(viewer).syntaxHighlight(); - - viewer.setAttribute('data-loaded', 'true'); - - this.$fileHolder.trigger('highlight:line'); - - this.toggleCopyButtonState(); - }); - } - switchToViewer(name) { - const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`); + const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`); if (this.activeViewer === newViewer) return; const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active'); const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`); - const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`); + const oldViewer = this.$fileHolder[0].querySelector(`.blob-viewer:not([data-type='${name}'])`); if (oldButton) { oldButton.classList.remove('active'); @@ -118,6 +109,40 @@ export default class BlobViewer { this.toggleCopyButtonState(); - this.loadViewer(newViewer); + BlobViewer.loadViewer(newViewer) + .then((viewer) => { + $(viewer).syntaxHighlight(); + + this.$fileHolder.trigger('highlight:line'); + + this.toggleCopyButtonState(); + }) + .catch(() => new Flash('Error loading viewer')); + } + + static loadViewer(viewerParam) { + const viewer = viewerParam; + const url = viewer.getAttribute('data-url'); + + return new Promise((resolve, reject) => { + if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { + resolve(viewer); + return; + } + + viewer.setAttribute('data-loading', 'true'); + + $.ajax({ + url, + dataType: 'JSON', + }) + .fail(reject) + .done((data) => { + viewer.innerHTML = data.html; + viewer.setAttribute('data-loaded', 'true'); + + resolve(viewer); + }); + }); } } diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb index 9faf68e6d97..4a6630dfd90 100644 --- a/app/controllers/concerns/renders_blob.rb +++ b/app/controllers/concerns/renders_blob.rb @@ -3,8 +3,11 @@ module RendersBlob def render_blob_json(blob) viewer = - if params[:viewer] == 'rich' + case params[:viewer] + when 'rich' blob.rich_viewer + when 'auxiliary' + blob.auxiliary_viewer else blob.simple_viewer end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8c26348a975..78b54dc20e5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -110,11 +110,8 @@ module ProjectsHelper end def license_short_name(project) - return 'LICENSE' if project.repository.license_key.nil? - - license = Licensee::License.new(project.repository.license_key) - - license.nickname || license.name + license = project.repository.license + license&.nickname || license&.name || 'LICENSE' end def last_push_event diff --git a/app/models/blob.rb b/app/models/blob.rb index 8452a96e909..63a81c0e3bd 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -34,10 +34,13 @@ class Blob < SimpleDelegator BlobViewer::BinarySTL, BlobViewer::TextSTL - ].freeze + ].sort_by { |v| v.binary? ? 0 : 1 }.freeze - BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze - TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze + AUXILIARY_VIEWERS = [ + BlobViewer::GitlabCiYml, + BlobViewer::RouteMap, + BlobViewer::License + ].freeze attr_reader :project @@ -154,6 +157,12 @@ class Blob < SimpleDelegator @rich_viewer = rich_viewer_class&.new(self) end + def auxiliary_viewer + return @auxiliary_viewer if defined?(@auxiliary_viewer) + + @auxiliary_viewer = auxiliary_viewer_class&.new(self) + end + def rendered_as_text?(ignore_errors: true) simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?) end @@ -180,17 +189,18 @@ class Blob < SimpleDelegator end def rich_viewer_class + viewer_class_from(RICH_VIEWERS) + end + + def auxiliary_viewer_class + viewer_class_from(AUXILIARY_VIEWERS) + end + + def viewer_class_from(classes) return if empty? || external_storage_error? - classes = - if stored_externally? - BINARY_VIEWERS + TEXT_VIEWERS - elsif binary? - BINARY_VIEWERS - else # text - TEXT_VIEWERS - end + verify_binary = !stored_externally? - classes.find { |viewer_class| viewer_class.can_render?(self) } + classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) } end end diff --git a/app/models/blob_viewer/auxiliary.rb b/app/models/blob_viewer/auxiliary.rb new file mode 100644 index 00000000000..db124397b27 --- /dev/null +++ b/app/models/blob_viewer/auxiliary.rb @@ -0,0 +1,12 @@ +module BlobViewer + module Auxiliary + extend ActiveSupport::Concern + + included do + self.loading_partial_name = 'loading_auxiliary' + self.type = :auxiliary + self.max_size = 100.kilobytes + self.absolute_max_size = 100.kilobytes + end + end +end diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb index a8b91d8d6bc..4f38c31714b 100644 --- a/app/models/blob_viewer/base.rb +++ b/app/models/blob_viewer/base.rb @@ -1,8 +1,12 @@ module BlobViewer class Base - class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size + PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze - delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class + class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_type, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size + + self.loading_partial_name = 'loading' + + delegate :partial_path, :loading_partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class attr_reader :blob attr_accessor :override_max_size @@ -12,7 +16,11 @@ module BlobViewer end def self.partial_path - "projects/blob/viewers/#{partial_name}" + File.join(PARTIAL_PATH_PREFIX, partial_name) + end + + def self.loading_partial_path + File.join(PARTIAL_PATH_PREFIX, loading_partial_name) end def self.rich? @@ -23,6 +31,10 @@ module BlobViewer type == :simple end + def self.auxiliary? + type == :auxiliary + end + def self.client_side? client_side end @@ -39,8 +51,12 @@ module BlobViewer !binary? end - def self.can_render?(blob) - !extensions || extensions.include?(blob.extension) + def self.can_render?(blob, verify_binary: true) + return false if verify_binary && binary? != blob.binary? + return true if extensions&.include?(blob.extension) + return true if file_type && Gitlab::FileDetector.type_of(blob.path) == file_type + + false end def too_large? @@ -83,9 +99,7 @@ module BlobViewer end def prepare! - if server_side? && blob.project - blob.load_all_data!(blob.project.repository) - end + # To be overridden by subclasses end private diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb new file mode 100644 index 00000000000..81afab2f49b --- /dev/null +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -0,0 +1,23 @@ +module BlobViewer + class GitlabCiYml < Base + include ServerSide + include Auxiliary + + self.partial_name = 'gitlab_ci_yml' + self.loading_partial_name = 'gitlab_ci_yml_loading' + self.file_type = :gitlab_ci + self.binary = false + + def validation_message + return @validation_message if defined?(@validation_message) + + prepare! + + @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data) + end + + def valid? + validation_message.blank? + end + end +end diff --git a/app/models/blob_viewer/license.rb b/app/models/blob_viewer/license.rb new file mode 100644 index 00000000000..3ad49570c88 --- /dev/null +++ b/app/models/blob_viewer/license.rb @@ -0,0 +1,23 @@ +module BlobViewer + class License < Base + # We treat the License viewer as if it renders the content client-side, + # so that it doesn't attempt to load the entire blob contents and is + # rendered synchronously instead of loaded asynchronously. + include ClientSide + include Auxiliary + + self.partial_name = 'license' + self.file_type = :license + self.binary = false + + def license + blob.project.repository.license + end + + def render_error + return if license + + :unknown_license + end + end +end diff --git a/app/models/blob_viewer/route_map.rb b/app/models/blob_viewer/route_map.rb new file mode 100644 index 00000000000..1ca730c1ea0 --- /dev/null +++ b/app/models/blob_viewer/route_map.rb @@ -0,0 +1,30 @@ +module BlobViewer + class RouteMap < Base + include ServerSide + include Auxiliary + + self.partial_name = 'route_map' + self.loading_partial_name = 'route_map_loading' + self.file_type = :route_map + self.binary = false + + def validation_message + return @validation_message if defined?(@validation_message) + + prepare! + + @validation_message = + begin + Gitlab::RouteMap.new(blob.data) + + nil + rescue Gitlab::RouteMap::FormatError => e + e.message + end + end + + def valid? + validation_message.blank? + end + end +end diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb index 899107d02ea..e8c5c17b824 100644 --- a/app/models/blob_viewer/server_side.rb +++ b/app/models/blob_viewer/server_side.rb @@ -7,5 +7,11 @@ module BlobViewer self.max_size = 2.megabytes self.absolute_max_size = 5.megabytes end + + def prepare! + if blob.project + blob.load_all_data!(blob.project.repository) + end + end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 9d9d94c4486..2aec7237640 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -549,6 +549,13 @@ class Repository end cache_method :license_key + def license + return @license if defined?(@license) + return unless license_key + + @license = Licensee::License.new(license_key) + end + def gitignore file_on_head(:gitignore) end @@ -1083,8 +1090,8 @@ class Repository def file_on_head(type) if head = tree(:head) - head.blobs.find do |file| - Gitlab::FileDetector.type_of(file.name) == type + head.blobs.find do |blob| + Gitlab::FileDetector.type_of(blob.path) == type end end end diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index f4307421ed0..8af945ddb2c 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -6,6 +6,11 @@ - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref + - auxiliary_viewer = blob.auxiliary_viewer + - if auxiliary_viewer && !auxiliary_viewer.render_error + .well-segment.blob-auxiliary-viewer + = render 'projects/blob/viewer', viewer: auxiliary_viewer + #blob-content-holder.blob-content-holder %article.file-holder = render "projects/blob/header", blob: blob diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 41187d5ce66..3d9c3a59980 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -5,8 +5,7 @@ - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously .blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) } - if load_asynchronously - .text-center.prepend-top-default.append-bottom-default - = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content') + = render viewer.loading_partial_path, viewer: viewer - elsif render_error = render 'projects/blob/render_error', viewer: viewer - else diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml new file mode 100644 index 00000000000..28c5be6ebf3 --- /dev/null +++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml @@ -0,0 +1,9 @@ +- if viewer.valid? + = icon('check fw') + This GitLab CI configuration is valid. +- else + = icon('warning fw') + This GitLab CI configuration is invalid: + = viewer.validation_message + += link_to 'Learn more', help_page_path('ci/yaml/README') diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml new file mode 100644 index 00000000000..10cbf6a2f7a --- /dev/null +++ b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml @@ -0,0 +1,4 @@ += icon('spinner spin fw') +Validating GitLab CI configuration… + += link_to 'Learn more', help_page_path('ci/yaml/README') diff --git a/app/views/projects/blob/viewers/_license.html.haml b/app/views/projects/blob/viewers/_license.html.haml new file mode 100644 index 00000000000..9a79d164692 --- /dev/null +++ b/app/views/projects/blob/viewers/_license.html.haml @@ -0,0 +1,8 @@ +- license = viewer.license + += icon('balance-scale fw') +This project is licensed under the += succeed '.' do + %strong= license.name + += link_to 'Learn more about this license', license.url, target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml new file mode 100644 index 00000000000..120c0540335 --- /dev/null +++ b/app/views/projects/blob/viewers/_loading.html.haml @@ -0,0 +1,2 @@ +.text-center.prepend-top-default.append-bottom-default + = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…') diff --git a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml new file mode 100644 index 00000000000..058c74bce0d --- /dev/null +++ b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml @@ -0,0 +1,2 @@ += icon('spinner spin fw') +Loading… diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml new file mode 100644 index 00000000000..d0fcd55f6c1 --- /dev/null +++ b/app/views/projects/blob/viewers/_route_map.html.haml @@ -0,0 +1,9 @@ +- if viewer.valid? + = icon('check fw') + This Route Map is valid. +- else + = icon('warning fw') + This Route Map is invalid: + = viewer.validation_message + += link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml new file mode 100644 index 00000000000..2318cf82f58 --- /dev/null +++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml @@ -0,0 +1,4 @@ += icon('spinner spin fw') +Validating Route Map… + += link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') diff --git a/doc/ci/environments.md b/doc/ci/environments.md index b28f3e13eae..bab765d1e12 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -442,7 +442,8 @@ and/or `production`) you can see this information in the merge request itself. ![Environment URLs in merge request](img/environments_link_url_mr.png) -### Go directly from source files to public pages on the environment +### Go directly from source files to public pages on the environment + > Introduced in GitLab 8.17. diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index c9ca4cadd1c..f8b3d0b4965 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -13,7 +13,8 @@ module Gitlab gitignore: '.gitignore', koding: '.koding.yml', gitlab_ci: '.gitlab-ci.yml', - avatar: /\Alogo\.(png|jpg|gif)\z/ + avatar: /\Alogo\.(png|jpg|gif)\z/, + route_map: 'route-map.yml' }.freeze # Returns an Array of file types based on the given paths. diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 5955623f565..9888624a509 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -5,13 +5,13 @@ feature 'File blob', :js, feature: true do def visit_blob(path, fragment = nil) visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) + + wait_for_ajax end context 'Ruby file' do before do visit_blob('files/ruby/popen.rb') - - wait_for_ajax end it 'displays the blob' do @@ -35,8 +35,6 @@ feature 'File blob', :js, feature: true do context 'visiting directly' do before do visit_blob('files/markdown/ruby-style-guide.md') - - wait_for_ajax end it 'displays the blob using the rich viewer' do @@ -104,8 +102,6 @@ feature 'File blob', :js, feature: true do context 'visiting with a line number anchor' do before do visit_blob('files/markdown/ruby-style-guide.md', 'L1') - - wait_for_ajax end it 'displays the blob using the simple viewer' do @@ -148,8 +144,6 @@ feature 'File blob', :js, feature: true do project.update_attribute(:lfs_enabled, true) visit_blob('files/lfs/file.md') - - wait_for_ajax end it 'displays an error' do @@ -198,8 +192,6 @@ feature 'File blob', :js, feature: true do context 'when LFS is disabled on the project' do before do visit_blob('files/lfs/file.md') - - wait_for_ajax end it 'displays the blob' do @@ -235,8 +227,6 @@ feature 'File blob', :js, feature: true do ).execute visit_blob('files/test.pdf') - - wait_for_ajax end it 'displays the blob' do @@ -263,8 +253,6 @@ feature 'File blob', :js, feature: true do project.update_attribute(:lfs_enabled, true) visit_blob('files/lfs/lfs_object.iso') - - wait_for_ajax end it 'displays the blob' do @@ -287,8 +275,6 @@ feature 'File blob', :js, feature: true do context 'when LFS is disabled on the project' do before do visit_blob('files/lfs/lfs_object.iso') - - wait_for_ajax end it 'displays the blob' do @@ -312,8 +298,6 @@ feature 'File blob', :js, feature: true do context 'ZIP file' do before do visit_blob('Gemfile.zip') - - wait_for_ajax end it 'displays the blob' do @@ -348,8 +332,6 @@ feature 'File blob', :js, feature: true do ).execute visit_blob('files/empty.md') - - wait_for_ajax end it 'displays an error' do @@ -369,4 +351,80 @@ feature 'File blob', :js, feature: true do end end end + + context '.gitlab-ci.yml' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + visit_blob('.gitlab-ci.yml') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that configuration is valid + expect(page).to have_content('This GitLab CI configuration is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context '.gitlab/route-map.yml' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab/route-map.yml", + file_path: '.gitlab/route-map.yml', + file_content: <<-MAP.strip_heredoc + # Team data + - source: 'data/team.yml' + public: 'team/' + MAP + ).execute + + visit_blob('.gitlab/route-map.yml') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that map is valid + expect(page).to have_content('This Route Map is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context 'LICENSE' do + before do + visit_blob('LICENSE') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows license + expect(page).to have_content('This project is licensed under the MIT License.') + + # shows a learn more link + expect(page).to have_link('Learn more about this license', 'http://choosealicense.com/licenses/mit/') + end + end + end end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index f84c6b48173..f19e1af65a6 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -271,6 +271,52 @@ describe Blob do end end + describe '#auxiliary_viewer' do + context 'when the blob has an external storage error' do + before do + project.lfs_enabled = false + end + + it 'returns nil' do + blob = fake_blob(path: 'LICENSE', lfs: true) + + expect(blob.auxiliary_viewer).to be_nil + end + end + + context 'when the blob is empty' do + it 'returns nil' do + blob = fake_blob(data: '') + + expect(blob.auxiliary_viewer).to be_nil + end + end + + context 'when the blob is stored externally' do + it 'returns a matching viewer' do + blob = fake_blob(path: 'LICENSE', lfs: true) + + expect(blob.auxiliary_viewer).to be_a(BlobViewer::License) + end + end + + context 'when the blob is binary' do + it 'returns nil' do + blob = fake_blob(path: 'LICENSE', binary: true) + + expect(blob.auxiliary_viewer).to be_nil + end + end + + context 'when the blob is text-based' do + it 'returns a matching text-based viewer' do + blob = fake_blob(path: 'LICENSE') + + expect(blob.auxiliary_viewer).to be_a(BlobViewer::License) + end + end + end + describe '#rendered_as_text?' do context 'when ignoring errors' do context 'when the simple viewer is text-based' do diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb index 740ad9d275e..a6641970e1b 100644 --- a/spec/models/blob_viewer/base_spec.rb +++ b/spec/models/blob_viewer/base_spec.rb @@ -8,6 +8,7 @@ describe BlobViewer::Base, model: true do let(:viewer_class) do Class.new(described_class) do self.extensions = %w(pdf) + self.binary = true self.max_size = 1.megabyte self.absolute_max_size = 5.megabytes self.client_side = false @@ -18,14 +19,47 @@ describe BlobViewer::Base, model: true do describe '.can_render?' do context 'when the extension is supported' do - let(:blob) { fake_blob(path: 'file.pdf') } + context 'when the binaryness matches' do + let(:blob) { fake_blob(path: 'file.pdf', binary: true) } - it 'returns true' do - expect(viewer_class.can_render?(blob)).to be_truthy + it 'returns true' do + expect(viewer_class.can_render?(blob)).to be_truthy + end + end + + context 'when the binaryness does not match' do + let(:blob) { fake_blob(path: 'file.pdf', binary: false) } + + it 'returns false' do + expect(viewer_class.can_render?(blob)).to be_falsey + end end end - context 'when the extension is not supported' do + context 'when the file type is supported' do + before do + viewer_class.file_type = :license + viewer_class.binary = false + end + + context 'when the binaryness matches' do + let(:blob) { fake_blob(path: 'LICENSE', binary: false) } + + it 'returns true' do + expect(viewer_class.can_render?(blob)).to be_truthy + end + end + + context 'when the binaryness does not match' do + let(:blob) { fake_blob(path: 'LICENSE', binary: true) } + + it 'returns false' do + expect(viewer_class.can_render?(blob)).to be_falsey + end + end + end + + context 'when the extension and file type are not supported' do let(:blob) { fake_blob(path: 'file.txt') } it 'returns false' do @@ -153,34 +187,4 @@ describe BlobViewer::Base, model: true do end end end - - describe '#prepare!' do - context 'when the viewer is server side' do - let(:blob) { fake_blob(path: 'file.md') } - - before do - viewer_class.client_side = false - end - - it 'loads all blob data' do - expect(blob).to receive(:load_all_data!) - - viewer.prepare! - end - end - - context 'when the viewer is client side' do - let(:blob) { fake_blob(path: 'file.md') } - - before do - viewer_class.client_side = true - end - - it "doesn't load all blob data" do - expect(blob).not_to receive(:load_all_data!) - - viewer.prepare! - end - end - end end diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb new file mode 100644 index 00000000000..0c6c24ece21 --- /dev/null +++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe BlobViewer::GitlabCiYml, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } + let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) } + subject { described_class.new(blob) } + + describe '#validation_message' do + it 'calls prepare! on the viewer' do + expect(subject).to receive(:prepare!) + + subject.validation_message + end + + context 'when the configuration is valid' do + it 'returns nil' do + expect(subject.validation_message).to be_nil + end + end + + context 'when the configuration is invalid' do + let(:data) { 'oof' } + + it 'returns the error message' do + expect(subject.validation_message).to eq('Invalid configuration format') + end + end + end +end diff --git a/spec/models/blob_viewer/license_spec.rb b/spec/models/blob_viewer/license_spec.rb new file mode 100644 index 00000000000..944ddd32b92 --- /dev/null +++ b/spec/models/blob_viewer/license_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe BlobViewer::License, model: true do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:blob) { fake_blob(path: 'LICENSE') } + subject { described_class.new(blob) } + + describe '#license' do + it 'returns the blob project repository license' do + expect(subject.license).not_to be_nil + expect(subject.license).to eq(project.repository.license) + end + end + + describe '#render_error' do + context 'when there is no license' do + before do + allow(project.repository).to receive(:license).and_return(nil) + end + + it 'returns :unknown_license' do + expect(subject.render_error).to eq(:unknown_license) + end + end + + context 'when there is a license' do + it 'returns nil' do + expect(subject.render_error).to be_nil + end + end + end +end diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb new file mode 100644 index 00000000000..4854e0262d9 --- /dev/null +++ b/spec/models/blob_viewer/route_map_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe BlobViewer::RouteMap, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-MAP.strip_heredoc + # Team data + - source: 'data/team.yml' + public: 'team/' + MAP + end + let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) } + subject { described_class.new(blob) } + + describe '#validation_message' do + it 'calls prepare! on the viewer' do + expect(subject).to receive(:prepare!) + + subject.validation_message + end + + context 'when the configuration is valid' do + it 'returns nil' do + expect(subject.validation_message).to be_nil + end + end + + context 'when the configuration is invalid' do + let(:data) { 'oof' } + + it 'returns the error message' do + expect(subject.validation_message).to eq('Route map is not an array') + end + end + end +end diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb new file mode 100644 index 00000000000..ddca9b79390 --- /dev/null +++ b/spec/models/blob_viewer/server_side_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::ServerSide, model: true do + include FakeBlobHelpers + + let(:project) { build(:empty_project) } + + let(:viewer_class) do + Class.new(BlobViewer::Base) do + include BlobViewer::ServerSide + end + end + + subject { viewer_class.new(blob) } + + describe '#prepare!' do + let(:blob) { fake_blob(path: 'file.txt') } + + it 'loads all blob data' do + expect(blob).to receive(:load_all_data!) + + subject.prepare! + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index cea8db7a926..6ebc9965924 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Repository, models: true do include RepoHelpers - TestBlob = Struct.new(:name) + TestBlob = Struct.new(:path) let(:project) { create(:project, :repository) } let(:repository) { project.repository } @@ -565,31 +565,31 @@ describe Repository, models: true do it 'accepts changelog' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) - expect(repository.changelog.name).to eq('changelog') + expect(repository.changelog.path).to eq('changelog') end it 'accepts news instead of changelog' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')]) - expect(repository.changelog.name).to eq('news') + expect(repository.changelog.path).to eq('news') end it 'accepts history instead of changelog' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')]) - expect(repository.changelog.name).to eq('history') + expect(repository.changelog.path).to eq('history') end it 'accepts changes instead of changelog' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')]) - expect(repository.changelog.name).to eq('changes') + expect(repository.changelog.path).to eq('changes') end it 'is case-insensitive' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')]) - expect(repository.changelog.name).to eq('CHANGELOG') + expect(repository.changelog.path).to eq('CHANGELOG') end end @@ -624,7 +624,7 @@ describe Repository, models: true do repository.create_file(user, 'LICENSE', 'Copyright!', message: 'Add LICENSE', branch_name: 'master') - expect(repository.license_blob.name).to eq('LICENSE') + expect(repository.license_blob.path).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| @@ -654,7 +654,7 @@ describe Repository, models: true do expect(repository.license_key).to be_nil end - it 'detects license file with no recognizable open-source license content' do + it 'returns nil when the content is not recognizable' do repository.create_file(user, 'LICENSE', 'Copyright!', message: 'Add LICENSE', branch_name: 'master') @@ -670,12 +670,45 @@ describe Repository, models: true do end end + describe '#license' do + before do + repository.delete_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') + end + + it 'returns nil when no license is detected' do + expect(repository.license).to be_nil + end + + it 'returns nil when the repository does not exist' do + expect(repository).to receive(:exists?).and_return(false) + + expect(repository.license).to be_nil + end + + it 'returns nil when the content is not recognizable' do + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') + + expect(repository.license).to be_nil + end + + it 'returns the license' do + license = Licensee::License.new('mit') + repository.create_file(user, 'LICENSE', + license.content, + message: 'Add LICENSE', branch_name: 'master') + + expect(repository.license).to eq(license) + end + end + describe "#gitlab_ci_yml", caching: true do it 'returns valid file' do files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')] expect(repository.tree).to receive(:blobs).and_return(files) - expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml') + expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml') end it 'returns nil if not exists' do diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index 501f90c5f9a..08018767624 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -47,10 +47,10 @@ describe 'projects/blob/_viewer.html.haml', :view do expect(rendered).to have_css('.blob-viewer[data-url]') end - it 'displays a spinner' do + it 'renders the loading indicator' do render_view - expect(rendered).to have_css('i[aria-label="Loading content"]') + expect(view).to render_template('projects/blob/viewers/_loading') end end