summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/viewer/index.js99
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/controllers/concerns/renders_blob.rb5
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/helpers/projects_helper.rb7
-rw-r--r--app/models/blob.rb34
-rw-r--r--app/models/blob_viewer/auxiliary.rb12
-rw-r--r--app/models/blob_viewer/base.rb30
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb23
-rw-r--r--app/models/blob_viewer/license.rb23
-rw-r--r--app/models/blob_viewer/route_map.rb30
-rw-r--r--app/models/blob_viewer/server_side.rb6
-rw-r--r--app/models/repository.rb27
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/blob/_blob.html.haml5
-rw-r--r--app/views/projects/blob/_viewer.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml9
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_license.html.haml8
-rw-r--r--app/views/projects/blob/viewers/_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_loading_auxiliary.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml9
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml4
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml6
-rw-r--r--changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml4
-rw-r--r--changelogs/unreleased/dm-auxiliary-viewers.yml5
-rw-r--r--changelogs/unreleased/issue_api_change.yml5
-rw-r--r--doc/ci/environments.md3
-rw-r--r--lib/gitlab/file_detector.rb3
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb31
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb98
-rw-r--r--spec/javascripts/notes_spec.js25
-rw-r--r--spec/javascripts/sidebar/sidebar_bundle_spec.js42
-rw-r--r--spec/models/blob_spec.rb46
-rw-r--r--spec/models/blob_viewer/base_spec.rb72
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb32
-rw-r--r--spec/models/blob_viewer/license_spec.rb34
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb38
-rw-r--r--spec/models/blob_viewer/server_side_spec.rb25
-rw-r--r--spec/models/repository_spec.rb54
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb4
43 files changed, 673 insertions, 209 deletions
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/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index f6fe6d9f0fd..7ba44835741 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -24,7 +24,7 @@ const normalizeNewlines = function(str) {
(function() {
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
- const REGEX_SLASH_COMMANDS = /\/\w+/g;
+ const REGEX_SLASH_COMMANDS = /^\/\w+/gm;
Notes.interval = null;
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/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 750c3ec486a..afbea3e2b40 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -38,6 +38,8 @@ class Projects::TagsController < Projects::ApplicationController
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
else
@error = result[:message]
+ @message = params[:message]
+ @release_description = params[:release_description]
render action: 'new'
end
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..11163ec77e9 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -30,7 +30,7 @@ class Repository
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :rendered_readme,
changelog: :changelog,
- license: %i(license_blob license_key),
+ license: %i(license_blob license_key license),
contributing: :contribution_guide,
gitignore: :gitignore,
koding: :koding_yml,
@@ -42,13 +42,13 @@ class Repository
# variable.
#
# This only works for methods that do not take any arguments.
- def self.cache_method(name, fallback: nil)
+ def self.cache_method(name, fallback: nil, memoize_only: false)
original = :"_uncached_#{name}"
alias_method(original, name)
define_method(name) do
- cache_method_output(name, fallback: fallback) { __send__(original) }
+ cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) }
end
end
@@ -549,6 +549,13 @@ class Repository
end
cache_method :license_key
+ def license
+ return unless license_key
+
+ Licensee::License.new(license_key)
+ end
+ cache_method :license, memoize_only: true
+
def gitignore
file_on_head(:gitignore)
end
@@ -1061,14 +1068,20 @@ class Repository
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
- def cache_method_output(key, fallback: nil, &block)
+ def cache_method_output(key, fallback: nil, memoize_only: false, &block)
ivar = cache_instance_variable_name(key)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
begin
- instance_variable_set(ivar, cache.fetch(key, &block))
+ value =
+ if memoize_only
+ yield
+ else
+ cache.fetch(key, &block)
+ end
+ instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
@@ -1083,8 +1096,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/_zen.html.haml b/app/views/projects/_zen.html.haml
index 0c8241053e7..3b3d08ddd3c 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,10 +1,11 @@
- @gfm_form = true
+- current_text ||= nil
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
- else
- = text_area_tag attr, nil, class: classes, placeholder: placeholder
+ = text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
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/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 7c607d2956b..cbf841762b7 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -22,14 +22,14 @@
.form-group
= label_tag :message, nil, class: 'control-label'
.col-sm-10
- = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5
+ = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5
.help-block Optionally, add a message to the tag.
%hr
.form-group
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here...", current_text: @release_description
= render 'shared/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 5c1156b06fb..87aae793966 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -29,6 +29,8 @@
- if note.system
%span.system-note-message
= note.redacted_note_html
+ .original-note-content.hidden
+ = note.note
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 708adbc38f1..183ed34fba1 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -1,9 +1,9 @@
-.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } }
+.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), "aria-labelledby": "custom-notifications-title" }
.modal-dialog
.modal-content
.modal-header
- %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
- %span{ aria: { hidden: "true" } } ×
+ %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } }
+ %span{ "aria-hidden": "true" } } ×
%h4#custom-notifications-title.modal-title
Custom notification events
diff --git a/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml b/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml
new file mode 100644
index 00000000000..aae760b0ef5
--- /dev/null
+++ b/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml
@@ -0,0 +1,4 @@
+---
+title: Keep input data after creating a tag that already exists
+merge_request: 11155
+author:
diff --git a/changelogs/unreleased/dm-auxiliary-viewers.yml b/changelogs/unreleased/dm-auxiliary-viewers.yml
new file mode 100644
index 00000000000..ba73a499115
--- /dev/null
+++ b/changelogs/unreleased/dm-auxiliary-viewers.yml
@@ -0,0 +1,5 @@
+---
+title: Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and
+ LICENSE blob pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_api_change.yml b/changelogs/unreleased/issue_api_change.yml
new file mode 100644
index 00000000000..3ad2d57317c
--- /dev/null
+++ b/changelogs/unreleased/issue_api_change.yml
@@ -0,0 +1,5 @@
+---
+title: 'Issue API change: assignee_id parameter and assignee object in a response
+ have been deprecated'
+merge_request:
+author:
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
+### <a name="route-map"></a>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/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 4c9adcabe34..349b948eaee 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard shortcuts', feature: true, js: true do
+feature 'Dashboard shortcuts', :feature, :js do
context 'logged in' do
before do
login_as :user
@@ -8,21 +8,21 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- check_page_title('Projects')
-
- find('body').native.send_key([:shift, 'I'])
+ find('body').send_keys([:shift, 'I'])
check_page_title('Issues')
- find('body').native.send_key([:shift, 'M'])
+ find('body').send_keys([:shift, 'M'])
check_page_title('Merge Requests')
- find('body').native.send_keys([:shift, 'T'])
+ find('body').send_keys([:shift, 'T'])
check_page_title('Todos')
+
+ find('body').send_keys([:shift, 'P'])
+
+ check_page_title('Projects')
end
end
@@ -32,17 +32,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- expect(page).to have_content('No projects found')
-
- find('body').native.send_keys([:shift, 'G'])
+ find('body').send_keys([:shift, 'G'])
+ find('.nothing-here-block')
expect(page).to have_content('No public groups')
- find('body').native.send_keys([:shift, 'S'])
+ find('body').send_keys([:shift, 'S'])
+ find('.nothing-here-block')
expect(page).to have_selector('.snippets-list-holder')
+
+ find('body').send_keys([:shift, 'P'])
+
+ find('.nothing-here-block')
+ expect(page).to have_content('No projects found')
end
end
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/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index cfd599f793e..be4605a5b89 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -376,13 +376,20 @@ import '~/notes';
this.notes = new Notes('', []);
});
- it('should return true when comment has slash commands', () => {
- const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
+ it('should return true when comment begins with a slash command', () => {
+ const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy();
});
+ it('should return false when comment does NOT begin with a slash command', () => {
+ const sampleComment = 'Hey, /unassign Merging this';
+ const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
+
+ expect(hasSlashCommands).toBeFalsy();
+ });
+
it('should return false when comment does NOT have any slash commands', () => {
const sampleComment = 'Looking good, Awesome!';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
@@ -392,14 +399,20 @@ import '~/notes';
});
describe('stripSlashCommands', () => {
- const REGEX_SLASH_COMMANDS = /\/\w+/g;
+ it('should strip slash commands from the comment which begins with a slash command', () => {
+ this.notes = new Notes();
+ const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this';
+ const stripedComment = this.notes.stripSlashCommands(sampleComment);
+
+ expect(stripedComment).not.toBe(sampleComment);
+ });
- it('should strip slash commands from the comment', () => {
+ it('should NOT strip string that has slashes within', () => {
this.notes = new Notes();
- const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
+ const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
- expect(REGEX_SLASH_COMMANDS.test(stripedComment)).toBeFalsy();
+ expect(stripedComment).toBe(sampleComment);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_bundle_spec.js b/spec/javascripts/sidebar/sidebar_bundle_spec.js
deleted file mode 100644
index 7760b34e071..00000000000
--- a/spec/javascripts/sidebar/sidebar_bundle_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-import SidebarBundleDomContentLoaded from '~/sidebar/sidebar_bundle';
-import SidebarTimeTracking from '~/sidebar/components/time_tracking/sidebar_time_tracking';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
-
-describe('sidebar bundle', () => {
- gl.sidebarOptions = Mock.mediator;
-
- beforeEach(() => {
- spyOn(SidebarTimeTracking.methods, 'listenForSlashCommands').and.callFake(() => { });
- preloadFixtures('issues/open-issue.html.raw');
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- loadFixtures('issues/open-issue.html.raw');
- spyOn(Vue.prototype, '$mount');
- SidebarBundleDomContentLoaded();
- this.mediator = new SidebarMediator();
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- });
-
- it('the mediator should be already defined with some data', () => {
- SidebarBundleDomContentLoaded();
-
- expect(this.mediator.store).toBeDefined();
- expect(this.mediator.service).toBeDefined();
- expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
- expect(this.mediator.store.rootPath).toEqual(Mock.mediator.rootPath);
- expect(this.mediator.store.endPoint).toEqual(Mock.mediator.endPoint);
- expect(this.mediator.store.editable).toEqual(Mock.mediator.editable);
- });
-
- it('the sidebar time tracking and assignees components to have been mounted', () => {
- expect(Vue.prototype.$mount).toHaveBeenCalledTimes(2);
- });
-});
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 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 is not supported' do
+ 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..00a9f2abeb9 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
@@ -1825,11 +1858,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
- with(%i(rendered_readme license_blob license_key))
+ with(%i(rendered_readme license_blob license_key license))
expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
+ expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license))
end
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