diff options
author | Ruben Davila <rdavila84@gmail.com> | 2017-05-02 13:20:41 -0500 |
---|---|---|
committer | Ruben Davila <rdavila84@gmail.com> | 2017-05-02 13:20:41 -0500 |
commit | bbfbcebdf69261bac6aa142f430719955e8e86b9 (patch) | |
tree | 25916a1a761cb6c8fcdac4b20b76017a7f4506d3 /app/models | |
parent | 3b82444eb7791e58e3e0ba2f08b8ccde48e3d4c6 (diff) | |
parent | 920d55b9f8afd35e16351fb57d671acf66092e89 (diff) | |
download | gitlab-ce-bbfbcebdf69261bac6aa142f430719955e8e86b9.tar.gz |
Merge branch 'master' into 28433-internationalise-cycle-analytics-page
Diffstat (limited to 'app/models')
45 files changed, 685 insertions, 228 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index dd1a6922968..cf042717c95 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base attr_accessor :domain_whitelist_raw, :domain_blacklist_raw + validates :uuid, presence: true + validates :session_expire_delay, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base end end + before_validation :ensure_uuid! before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token @@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base private + def ensure_uuid! + return if uuid? + + self.uuid = SecureRandom.uuid + end + def check_repository_storages invalid = repository_storages - Gitlab.config.repositories.storages.keys errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless diff --git a/app/models/blob.rb b/app/models/blob.rb index 55872acef51..1cdb8811cff 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -3,8 +3,42 @@ class Blob < SimpleDelegator CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour - # The maximum size of an SVG that can be displayed. - MAXIMUM_SVG_SIZE = 2.megabytes + MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte + + # Finding a viewer for a blob happens based only on extension and whether the + # blob is binary or text, which means 1 blob should only be matched by 1 viewer, + # and the order of these viewers doesn't really matter. + # + # However, when the blob is an LFS pointer, we cannot know for sure whether the + # file being pointed to is binary or text. In this case, we match only on + # extension, preferring binary viewers over text ones if both exist, since the + # large files referred to in "Large File Storage" are much more likely to be + # binary than text. + # + # `.stl` files, for example, exist in both binary and text forms, and are + # handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob + # type. LFS pointers to `.stl` files are assumed to always be the binary kind, + # and use the `BinarySTL` viewer. + RICH_VIEWERS = [ + BlobViewer::Markup, + BlobViewer::Notebook, + BlobViewer::SVG, + + BlobViewer::Image, + BlobViewer::Sketch, + + BlobViewer::Video, + + BlobViewer::PDF, + + BlobViewer::BinarySTL, + BlobViewer::TextSTL, + ].freeze + + BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze + TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze + + attr_reader :project # Wrap a Gitlab::Git::Blob object, or return nil when given nil # @@ -16,10 +50,16 @@ class Blob < SimpleDelegator # # blob = Blob.decorate(nil) # puts "truthy" if blob # No output - def self.decorate(blob) + def self.decorate(blob, project = nil) return if blob.nil? - new(blob) + new(blob, project) + end + + def initialize(blob, project = nil) + @project = project + + super(blob) end # Returns the data of the blob. @@ -35,82 +75,107 @@ class Blob < SimpleDelegator end def no_highlighting? - size && size > 1.megabyte + size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE end - def only_display_raw? + def too_large? size && truncated? end + # Returns the size of the file that this blob represents. If this blob is an + # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is + # the size of the blob itself. + def raw_size + if valid_lfs_pointer? + lfs_size + else + size + end + end + + # Returns whether the file that this blob represents is binary. If this blob is + # an LFS pointer, we assume the file stored in LFS is binary, unless a + # text-based rich blob viewer matched on the file's extension. Otherwise, this + # depends on the type of the blob itself. + def raw_binary? + if valid_lfs_pointer? + if rich_viewer + rich_viewer.binary? + else + true + end + else + binary? + end + end + def extension - extname.downcase.delete('.') + @extension ||= extname.downcase.delete('.') end - def svg? - text? && language && language.name == 'SVG' + def video? + UploaderHelper::VIDEO_EXT.include?(extension) end - def pdf? - extension == 'pdf' + def readable_text? + text? && !valid_lfs_pointer? && !too_large? end - def ipython_notebook? - text? && language&.name == 'Jupyter Notebook' + def valid_lfs_pointer? + lfs_pointer? && project&.lfs_enabled? end - def sketch? - binary? && extension == 'sketch' + def invalid_lfs_pointer? + lfs_pointer? && !project&.lfs_enabled? end - def stl? - extension == 'stl' + def simple_viewer + @simple_viewer ||= simple_viewer_class.new(self) end - def markup? - text? && Gitlab::MarkupHelper.markup?(name) + def rich_viewer + return @rich_viewer if defined?(@rich_viewer) + + @rich_viewer = rich_viewer_class&.new(self) end - def size_within_svg_limits? - size <= MAXIMUM_SVG_SIZE + def rendered_as_text?(ignore_errors: true) + simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?) end - def video? - UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) + def show_viewer_switcher? + rendered_as_text? && rich_viewer end - def to_partial_path(project) - if lfs_pointer? - if project.lfs_enabled? - 'download' - else - 'text' - end - elsif image? - 'image' - elsif svg? - 'svg' - elsif pdf? - 'pdf' - elsif ipython_notebook? - 'notebook' - elsif sketch? - 'sketch' - elsif stl? - 'stl' - elsif markup? - if only_display_raw? - 'too_large' - else - 'markup' - end - elsif text? - if only_display_raw? - 'too_large' - else - 'text' - end - else - 'download' + def override_max_size! + simple_viewer&.override_max_size = true + rich_viewer&.override_max_size = true + end + + private + + def simple_viewer_class + if empty? + BlobViewer::Empty + elsif raw_binary? + BlobViewer::Download + else # text + BlobViewer::Text end end + + def rich_viewer_class + return if invalid_lfs_pointer? || empty? + + classes = + if valid_lfs_pointer? + BINARY_VIEWERS + TEXT_VIEWERS + elsif binary? + BINARY_VIEWERS + else # text + TEXT_VIEWERS + end + + classes.find { |viewer_class| viewer_class.can_render?(self) } + end end diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb new file mode 100644 index 00000000000..f944b00c9d3 --- /dev/null +++ b/app/models/blob_viewer/base.rb @@ -0,0 +1,96 @@ +module BlobViewer + class Base + class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size + + delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class + + attr_reader :blob + attr_accessor :override_max_size + + def initialize(blob) + @blob = blob + end + + def self.partial_path + "projects/blob/viewers/#{partial_name}" + end + + def self.rich? + type == :rich + end + + def self.simple? + type == :simple + end + + def self.client_side? + client_side + end + + def self.server_side? + !client_side? + end + + def self.binary? + binary + end + + def self.text? + !binary? + end + + def self.can_render?(blob) + !extensions || extensions.include?(blob.extension) + end + + def too_large? + blob.raw_size > max_size + end + + def absolutely_too_large? + blob.raw_size > absolute_max_size + end + + def can_override_max_size? + too_large? && !absolutely_too_large? + end + + # This method is used on the server side to check whether we can attempt to + # render the blob at all. Human-readable error messages are found in the + # `BlobHelper#blob_render_error_reason` helper. + # + # This method does not and should not load the entire blob contents into + # memory, and should not be overridden to do so in order to validate the + # format of the blob. + # + # Prefer to implement a client-side viewer, where the JS component loads the + # binary from `blob_raw_url` and does its own format validation and error + # rendering, especially for potentially large binary formats. + def render_error + return @render_error if defined?(@render_error) + + @render_error = + if server_side_but_stored_in_lfs? + # Files stored in LFS can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `blob_raw_url` using AJAX. + :server_side_but_stored_in_lfs + elsif override_max_size ? absolutely_too_large? : too_large? + :too_large + end + end + + def prepare! + if server_side? && blob.project + blob.load_all_data!(blob.project.repository) + end + end + + private + + def server_side_but_stored_in_lfs? + server_side? && blob.valid_lfs_pointer? + end + end +end diff --git a/app/models/blob_viewer/binary_stl.rb b/app/models/blob_viewer/binary_stl.rb new file mode 100644 index 00000000000..80393471ef2 --- /dev/null +++ b/app/models/blob_viewer/binary_stl.rb @@ -0,0 +1,10 @@ +module BlobViewer + class BinarySTL < Base + include Rich + include ClientSide + + self.partial_name = 'stl' + self.extensions = %w(stl) + self.binary = true + end +end diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb new file mode 100644 index 00000000000..42ec68f864b --- /dev/null +++ b/app/models/blob_viewer/client_side.rb @@ -0,0 +1,11 @@ +module BlobViewer + module ClientSide + extend ActiveSupport::Concern + + included do + self.client_side = true + self.max_size = 10.megabytes + self.absolute_max_size = 50.megabytes + end + end +end diff --git a/app/models/blob_viewer/download.rb b/app/models/blob_viewer/download.rb new file mode 100644 index 00000000000..adc06587f69 --- /dev/null +++ b/app/models/blob_viewer/download.rb @@ -0,0 +1,17 @@ +module BlobViewer + class Download < Base + include Simple + # We treat the Download 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 + + self.partial_name = 'download' + self.binary = true + + # We can always render the Download viewer, even if the blob is in LFS or too large. + def render_error + nil + end + end +end diff --git a/app/models/blob_viewer/empty.rb b/app/models/blob_viewer/empty.rb new file mode 100644 index 00000000000..d9d128eb273 --- /dev/null +++ b/app/models/blob_viewer/empty.rb @@ -0,0 +1,9 @@ +module BlobViewer + class Empty < Base + include Simple + include ServerSide + + self.partial_name = 'empty' + self.binary = true + end +end diff --git a/app/models/blob_viewer/image.rb b/app/models/blob_viewer/image.rb new file mode 100644 index 00000000000..c4eae5c79c2 --- /dev/null +++ b/app/models/blob_viewer/image.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Image < Base + include Rich + include ClientSide + + self.partial_name = 'image' + self.extensions = UploaderHelper::IMAGE_EXT + self.binary = true + self.switcher_icon = 'picture-o' + self.switcher_title = 'image' + end +end diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb new file mode 100644 index 00000000000..8fdbab30dd1 --- /dev/null +++ b/app/models/blob_viewer/markup.rb @@ -0,0 +1,10 @@ +module BlobViewer + class Markup < Base + include Rich + include ServerSide + + self.partial_name = 'markup' + self.extensions = Gitlab::MarkupHelper::EXTENSIONS + self.binary = false + end +end diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb new file mode 100644 index 00000000000..8632b8a9885 --- /dev/null +++ b/app/models/blob_viewer/notebook.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Notebook < Base + include Rich + include ClientSide + + self.partial_name = 'notebook' + self.extensions = %w(ipynb) + self.binary = false + self.switcher_icon = 'file-text-o' + self.switcher_title = 'notebook' + end +end diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb new file mode 100644 index 00000000000..65805f5f388 --- /dev/null +++ b/app/models/blob_viewer/pdf.rb @@ -0,0 +1,12 @@ +module BlobViewer + class PDF < Base + include Rich + include ClientSide + + self.partial_name = 'pdf' + self.extensions = %w(pdf) + self.binary = true + self.switcher_icon = 'file-pdf-o' + self.switcher_title = 'PDF' + end +end diff --git a/app/models/blob_viewer/rich.rb b/app/models/blob_viewer/rich.rb new file mode 100644 index 00000000000..be373dbc948 --- /dev/null +++ b/app/models/blob_viewer/rich.rb @@ -0,0 +1,11 @@ +module BlobViewer + module Rich + extend ActiveSupport::Concern + + included do + self.type = :rich + self.switcher_icon = 'file-text-o' + self.switcher_title = 'rendered file' + end + end +end diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb new file mode 100644 index 00000000000..899107d02ea --- /dev/null +++ b/app/models/blob_viewer/server_side.rb @@ -0,0 +1,11 @@ +module BlobViewer + module ServerSide + extend ActiveSupport::Concern + + included do + self.client_side = false + self.max_size = 2.megabytes + self.absolute_max_size = 5.megabytes + end + end +end diff --git a/app/models/blob_viewer/simple.rb b/app/models/blob_viewer/simple.rb new file mode 100644 index 00000000000..454a20495fc --- /dev/null +++ b/app/models/blob_viewer/simple.rb @@ -0,0 +1,11 @@ +module BlobViewer + module Simple + extend ActiveSupport::Concern + + included do + self.type = :simple + self.switcher_icon = 'code' + self.switcher_title = 'source' + end + end +end diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb new file mode 100644 index 00000000000..818456778e1 --- /dev/null +++ b/app/models/blob_viewer/sketch.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Sketch < Base + include Rich + include ClientSide + + self.partial_name = 'sketch' + self.extensions = %w(sketch) + self.binary = true + self.switcher_icon = 'file-image-o' + self.switcher_title = 'preview' + end +end diff --git a/app/models/blob_viewer/svg.rb b/app/models/blob_viewer/svg.rb new file mode 100644 index 00000000000..b7e5cd71e6b --- /dev/null +++ b/app/models/blob_viewer/svg.rb @@ -0,0 +1,12 @@ +module BlobViewer + class SVG < Base + include Rich + include ServerSide + + self.partial_name = 'svg' + self.extensions = %w(svg) + self.binary = false + self.switcher_icon = 'picture-o' + self.switcher_title = 'image' + end +end diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb new file mode 100644 index 00000000000..e27b2c2b493 --- /dev/null +++ b/app/models/blob_viewer/text.rb @@ -0,0 +1,11 @@ +module BlobViewer + class Text < Base + include Simple + include ServerSide + + self.partial_name = 'text' + self.binary = false + self.max_size = 1.megabyte + self.absolute_max_size = 10.megabytes + end +end diff --git a/app/models/blob_viewer/text_stl.rb b/app/models/blob_viewer/text_stl.rb new file mode 100644 index 00000000000..8184dc0104c --- /dev/null +++ b/app/models/blob_viewer/text_stl.rb @@ -0,0 +1,5 @@ +module BlobViewer + class TextSTL < BinarySTL + self.binary = false + end +end diff --git a/app/models/blob_viewer/video.rb b/app/models/blob_viewer/video.rb new file mode 100644 index 00000000000..057f9fe516f --- /dev/null +++ b/app/models/blob_viewer/video.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Video < Base + include Rich + include ClientSide + + self.partial_name = 'video' + self.extensions = UploaderHelper::VIDEO_EXT + self.binary = true + self.switcher_icon = 'film' + self.switcher_title = 'video' + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 445247f1b41..4be4aa9ffe2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -75,29 +75,32 @@ module Ci pipeline.update_duration end + before_transition any => [:manual] do |pipeline| + pipeline.update_duration + end + before_transition canceled: any - [:canceled] do |pipeline| pipeline.auto_canceled_by = nil end after_transition [:created, :pending] => :running do |pipeline| - pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } + pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end after_transition any => [:success] do |pipeline| - pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } + pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end after_transition [:created, :pending, :running] => :success do |pipeline| - pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) } + pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) } end after_transition do |pipeline, transition| next if transition.loopback? pipeline.run_after_commit do - PipelineHooksWorker.perform_async(id) - Ci::ExpirePipelineCacheService.new(project, nil) - .execute(pipeline) + PipelineHooksWorker.perform_async(pipeline.id) + ExpirePipelineCacheWorker.perform_async(pipeline.id) end end @@ -385,6 +388,11 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end + # All the merge requests for which the current pipeline runs/ran against + def all_merge_requests + @all_merge_requests ||= project.merge_requests.where(source_branch: ref) + end + def detailed_status(current_user) Gitlab::Ci::Status::Pipeline::Factory .new(self, current_user) diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b8b3f00202..bb4cb8efd15 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -316,7 +316,7 @@ class Commit def uri_type(path) entry = @raw.tree.path(path) if entry[:type] == :blob - blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name])) + blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob.image? || blob.video? ? :raw : :blob else entry[:type] diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 8ea95beed79..eb32bf3d32a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -8,6 +8,14 @@ # # Corresponding foo_html, bar_html and baz_html fields should exist. module CacheMarkdownField + extend ActiveSupport::Concern + + # Increment this number every time the renderer changes its output + CACHE_VERSION = 1 + + # changes to these attributes cause the cache to be invalidates + INVALIDATED_BY = %w[author project].freeze + # Knows about the relationship between markdown and html field names, and # stores the rendering contexts for the latter class FieldData @@ -30,60 +38,74 @@ module CacheMarkdownField end end - # Dynamic registries don't really work in Rails as it's not guaranteed that - # every class will be loaded, so hardcode the list. - CACHING_CLASSES = %w[ - AbuseReport - Appearance - ApplicationSetting - BroadcastMessage - Issue - Label - MergeRequest - Milestone - Namespace - Note - Project - Release - Snippet - ].freeze - - def self.caching_classes - CACHING_CLASSES.map(&:constantize) - end - def skip_project_check? false end - extend ActiveSupport::Concern + # Returns the default Banzai render context for the cached markdown field. + def banzai_render_context(field) + raise ArgumentError.new("Unknown field: #{field.inspect}") unless + cached_markdown_fields.markdown_fields.include?(field) - included do - cattr_reader :cached_markdown_fields do - FieldData.new - end + # Always include a project key, or Banzai complains + project = self.project if self.respond_to?(:project) + context = cached_markdown_fields[field].merge(project: project) - # Returns the default Banzai render context for the cached markdown field. - def banzai_render_context(field) - raise ArgumentError.new("Unknown field: #{field.inspect}") unless - cached_markdown_fields.markdown_fields.include?(field) + # Banzai is less strict about authors, so don't always have an author key + context[:author] = self.author if self.respond_to?(:author) - # Always include a project key, or Banzai complains - project = self.project if self.respond_to?(:project) - context = cached_markdown_fields[field].merge(project: project) + context + end - # Banzai is less strict about authors, so don't always have an author key - context[:author] = self.author if self.respond_to?(:author) + # Update every column in a row if any one is invalidated, as we only store + # one version per row + def refresh_markdown_cache!(do_update: false) + options = { skip_project_check: skip_project_check? } - context - end + updates = cached_markdown_fields.markdown_fields.map do |markdown_field| + [ + cached_markdown_fields.html_field(markdown_field), + Banzai::Renderer.cacheless_render_field(self, markdown_field, options) + ] + end.to_h + updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION + + updates.each {|html_field, data| write_attribute(html_field, data) } + + update_columns(updates) if persisted? && do_update + end + + def cached_html_up_to_date?(markdown_field) + html_field = cached_markdown_fields.html_field(markdown_field) + + cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? + return false unless cached - # Allow callers to look up the cache field name, rather than hardcoding it - def markdown_cache_field_for(field) - raise ArgumentError.new("Unknown field: #{field}") unless - cached_markdown_fields.markdown_fields.include?(field) + markdown_changed = attribute_changed?(markdown_field) || false + html_changed = attribute_changed?(html_field) || false - cached_markdown_fields.html_field(field) + CacheMarkdownField::CACHE_VERSION == cached_markdown_version && + (html_changed || markdown_changed == html_changed) + end + + def invalidated_markdown_cache? + cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) } + end + + def attribute_invalidated?(attr) + __send__("#{attr}_invalidated?") + end + + def cached_html_for(markdown_field) + raise ArgumentError.new("Unknown field: #{field}") unless + cached_markdown_fields.markdown_fields.include?(markdown_field) + + __send__(cached_markdown_fields.html_field(markdown_field)) + end + + included do + cattr_reader :cached_markdown_fields do + FieldData.new end # Always exclude _html fields from attributes (including serialization). @@ -92,12 +114,18 @@ module CacheMarkdownField def attributes attrs = attributes_before_markdown_cache + attrs.delete('cached_markdown_version') + cached_markdown_fields.html_fields.each do |field| attrs.delete(field) end attrs end + + # Using before_update here conflicts with elasticsearch-model somehow + before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache? + before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache? end class_methods do @@ -107,31 +135,18 @@ module CacheMarkdownField # a corresponding _html field. Any custom rendering options may be provided # as a context. def cache_markdown_field(markdown_field, context = {}) - raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless - CacheMarkdownField::CACHING_CLASSES.include?(self.to_s) - cached_markdown_fields[markdown_field] = context html_field = cached_markdown_fields.html_field(markdown_field) - cache_method = "#{markdown_field}_cache_refresh".to_sym invalidation_method = "#{html_field}_invalidated?".to_sym - define_method(cache_method) do - options = { skip_project_check: skip_project_check? } - html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options) - __send__("#{html_field}=", html) - true - end - # The HTML becomes invalid if any dependent fields change. For now, assume # author and project invalidate the cache in all circumstances. define_method(invalidation_method) do changed_fields = changed_attributes.keys - invalidations = changed_fields & [markdown_field.to_s, "author", "project"] - !invalidations.empty? + invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY] + !invalidations.empty? || !cached_html_up_to_date?(markdown_field) end - - before_save cache_method, if: invalidation_method end end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index aca99feee53..b28e05d0c28 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -163,7 +163,20 @@ module Routable end end + # Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path, + # a new instance is instantiated, and we end up duplicating the same query to retrieve + # the route. Caching this per request ensures that even if we have multiple instances, + # we will not have to duplicate work, avoiding N+1 queries in some cases. def full_path + return uncached_full_path unless RequestStore.active? + + key = "routable/full_path/#{self.class.name}/#{self.id}" + RequestStore[key] ||= uncached_full_path + end + + private + + def uncached_full_path if route && route.path.present? @full_path ||= route.path else @@ -173,8 +186,6 @@ module Routable end end - private - def full_name_changed? name_changed? || parent_changed? end diff --git a/app/models/event.rb b/app/models/event.rb index 5c34844b5d3..b780c1faf81 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -16,7 +16,7 @@ class Event < ActiveRecord::Base RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour - delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true + delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true diff --git a/app/models/group.rb b/app/models/group.rb index 106084175ff..cbc10b00cf5 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -125,7 +125,7 @@ class Group < Namespace end def add_users(users, access_level, current_user: nil, expires_at: nil) - GroupMember.add_users_to_group( + GroupMember.add_users( self, users, access_level, diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb index c3f21c55240..6be8ca45739 100644 --- a/app/models/individual_note_discussion.rb +++ b/app/models/individual_note_discussion.rb @@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion def individual_note? true end + + def reply_attributes + super.tap { |attrs| attrs.delete(:discussion_id) } + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index d39ae3a6c92..305fc01f041 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -199,7 +199,7 @@ class Issue < ActiveRecord::Base # Returns `true` if the current issue can be viewed by either a logged in User # or an anonymous user. def visible_to_user?(user = nil) - return false unless project.feature_available?(:issues, user) + return false unless project && project.feature_available?(:issues, user) user ? readable_by?(user) : publicly_visible? end diff --git a/app/models/label.rb b/app/models/label.rb index d8b0e250732..ddddb6bdf8f 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -34,6 +34,7 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } + scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) } def self.prioritized(project) joins(:priorities) diff --git a/app/models/member.rb b/app/models/member.rb index 0545bd4eedf..7228e82e978 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -151,6 +151,27 @@ class Member < ActiveRecord::Base member end + def add_users(source, users, access_level, current_user: nil, expires_at: nil) + return [] unless users.present? + + # Collect all user ids into separate array + # so we can use single sql query to get user objects + user_ids = users.select { |user| user =~ /\A\d+\Z/ } + users = users - user_ids + User.where(id: user_ids) + + self.transaction do + users.map do |user| + add_user( + source, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) + end + end + end + def access_levels Gitlab::Access.sym_options end @@ -173,18 +194,6 @@ class Member < ActiveRecord::Base # There is no current user for bulk actions, in which case anything is allowed !current_user || current_user.can?(:"update_#{member.type.underscore}", member) end - - def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil) - users.each do |user| - add_user( - source, - user, - access_level, - current_user: current_user, - expires_at: expires_at - ) - end - end end def real_source_type diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 483425cd30f..28e10bc6172 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -21,18 +21,6 @@ class GroupMember < Member Gitlab::Access.sym_options_with_owner end - def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil) - self.transaction do - add_users_to_source( - group, - users, - access_level, - current_user: current_user, - expires_at: expires_at - ) - end - end - def group source end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 912820b51ac..b3a91feb091 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -16,7 +16,7 @@ class ProjectMember < Member before_destroy :delete_member_todos class << self - # Add users to project teams with passed access option + # Add users to projects with passed access option # # access can be an integer representing a access code # or symbol like :master representing role @@ -39,7 +39,7 @@ class ProjectMember < Member project_ids.each do |project_id| project = Project.find(project_id) - add_users_to_source( + add_users( project, users, access_level, diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1d4827375d7..365fa4f1e70 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? + validate :validate_target_project, on: :create scope :by_source_or_target_branch, ->(branch_name) do where("source_branch = :branch OR target_branch = :branch", branch: branch_name) @@ -191,22 +192,23 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args) end - def diffs(diff_options = nil) + def diffs(diff_options = {}) if compare - compare.diffs(diff_options) + # When saving MR diffs, `no_collapse` is implicitly added (because we need + # to save the entire contents to the DB), so add that here for + # consistency. + compare.diffs(diff_options.merge(no_collapse: true)) else merge_request_diff.diffs(diff_options) end end def diff_size - # The `#diffs` method ends up at an instance of a class inheriting from - # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults - # here too, to get the same diff size without performing highlighting. - # - opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {}) + # Calling `merge_request_diff.diffs.real_size` will also perform + # highlighting, which we don't need here. + return real_size if merge_request_diff - raw_diffs(opts).size + diffs.real_size end def diff_base_commit @@ -329,6 +331,12 @@ class MergeRequest < ActiveRecord::Base end end + def validate_target_project + return true if target_project.merge_requests_enabled? + + errors.add :base, 'Target project has disabled merge requests' + end + def validate_fork return true unless target_project && source_project return true if target_project == source_project diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 6604af2b47e..f0a3c30ea74 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -260,7 +260,7 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:state] = :empty else diff_collection = compare.diffs(Commit.max_diff_options) - new_attributes[:real_size] = compare.diffs.real_size + new_attributes[:real_size] = diff_collection.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 9bfa731785f..397dc7a25ab 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base validates :path, presence: true, length: { maximum: 255 }, - namespace: true + dynamic_path: true validate :nesting_level_allowed @@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + def has_parent? + parent.present? + end + private def repository_storage_paths diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 0bbc9451ffd..59737bb6085 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -107,7 +107,8 @@ module Network def find_commits(skip = 0) opts = { max_count: self.class.max_count, - skip: skip + skip: skip, + order: :date } opts[:ref] = @commit.id if @filter_ref diff --git a/app/models/out_of_context_discussion.rb b/app/models/out_of_context_discussion.rb index 85794630f70..4227c40b69a 100644 --- a/app/models/out_of_context_discussion.rb +++ b/app/models/out_of_context_discussion.rb @@ -15,8 +15,12 @@ class OutOfContextDiscussion < Discussion def self.override_discussion_id(note) discussion_id(note) end - + def self.note_class Note end + + def reply_attributes + super.tap { |attrs| attrs.delete(:discussion_id) } + end end diff --git a/app/models/project.rb b/app/models/project.rb index a160efba912..6a8f8c3500f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -74,6 +74,7 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + attr_writer :pipeline_status alias_attribute :title, :name @@ -181,7 +182,7 @@ class Project < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true - delegate :add_user, to: :team + delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :empty_repo?, to: :repository @@ -195,13 +196,14 @@ class Project < ActiveRecord::Base message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, - project_path: true, + dynamic_path: true, length: { maximum: 255 }, format: { with: Gitlab::Regex.project_path_regex, - message: Gitlab::Regex.project_path_regex_message } + message: Gitlab::Regex.project_path_regex_message }, + uniqueness: { scope: :namespace_id } + validates :namespace, presence: true validates :name, uniqueness: { scope: :namespace_id } - validates :path, uniqueness: { scope: :namespace_id } validates :import_url, addressable_url: true, if: :external_import? validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } @@ -1181,6 +1183,7 @@ class Project < ActiveRecord::Base end end + # Lazy loading of the `pipeline_status` attribute def pipeline_status @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) end @@ -1312,6 +1315,14 @@ class Project < ActiveRecord::Base namespace_id_changed? end + def default_merge_request_target + if forked_from_project&.merge_requests_enabled? + forked_from_project + else + self + end + end + alias_method :name_with_namespace, :full_name alias_method :human_name, :full_name alias_method :path_with_namespace, :full_path diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index f2dfb87dbda..fa782c6fbb7 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -22,7 +22,7 @@ class ChatNotificationService < Service end def can_test? - super && valid? + valid? end def self.supported_events diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 6d6644053f8..543b9b293e0 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -50,8 +50,8 @@ class ProjectTeam end def add_users(users, access_level, current_user: nil, expires_at: nil) - ProjectMember.add_users_to_projects( - [project.id], + ProjectMember.add_users( + project, users, access_level, current_user: current_user, diff --git a/app/models/repository.rb b/app/models/repository.rb index 7bb874d7744..ba34d570dbd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -17,9 +17,9 @@ class Repository # same name. The cache key used by those methods must also match method's # name. # - # For example, for entry `:readme` there's a method called `readme` which - # stores its data in the `readme` cache key. - CACHED_METHODS = %i(size commit_count readme contribution_guide + # For example, for entry `:commit_count` there's a method called `commit_count` which + # stores its data in the `commit_count` cache key. + CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide changelog license_blob license_key gitignore koding_yml gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? empty? root_ref).freeze @@ -28,7 +28,7 @@ class Repository # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # the corresponding methods to call for refreshing caches. METHOD_CACHES_FOR_FILE_TYPES = { - readme: :readme, + readme: :rendered_readme, changelog: :changelog, license: %i(license_blob license_key), contributing: :contribution_guide, @@ -450,7 +450,7 @@ class Repository def blob_at(sha, path) unless Gitlab::Git.blank_ref?(sha) - Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) + Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project) end rescue Gitlab::Git::Repository::NoRepository nil @@ -505,14 +505,8 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - def branch_count - branches.size - end + delegate :branch_count, :tag_count, to: :raw_repository cache_method :branch_count, fallback: 0 - - def tag_count - raw_repository.rugged.tags.count - end cache_method :tag_count, fallback: 0 def avatar @@ -527,7 +521,11 @@ class Repository head.readme end end - cache_method :readme + + def rendered_readme + MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme + end + cache_method :rendered_readme def contribution_guide file_on_head(:contributing) @@ -957,15 +955,13 @@ class Repository end def is_ancestor?(ancestor_id, descendant_id) - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved - # Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| - # if is_enabled - # raw_repository.is_ancestor?(ancestor_id, descendant_id) - # else - merge_base_commit(ancestor_id, descendant_id) == ancestor_id - # end - # end + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| + if is_enabled + raw_repository.is_ancestor?(ancestor_id, descendant_id) + else + merge_base_commit(ancestor_id, descendant_id) == ancestor_id + end + end end def empty_repo? diff --git a/app/models/service.rb b/app/models/service.rb index dc76bf925d3..c71a7d169ec 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -26,6 +26,7 @@ class Service < ActiveRecord::Base has_one :service_hook validates :project_id, presence: true, unless: proc { |service| service.template? } + validates :type, presence: true scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :issue_trackers, -> { where(category: 'issue_tracker') } @@ -131,7 +132,7 @@ class Service < ActiveRecord::Base end def can_test? - !project.empty_repo? + true end # reason why service cannot be tested diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 380835707e8..d8860718cb5 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,6 +1,5 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel - include Linguist::BlobHelper include CacheMarkdownField include Noteable include Participable @@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base ] end - def data - content + def blob + @blob ||= Blob.decorate(SnippetBlob.new(self), nil) end def hook_attrs attributes end - def size - 0 - end - def file_name super.to_s end - # alias for compatibility with blobs and highlighting - def path - file_name - end - - def name - file_name - end - def sanitized_file_name file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') end - def mode - nil - end - def visibility_level_field :visibility_level end - def no_highlighting? - content.lines.count > 1000 - end - def notes_with_associations notes.includes(:author) end diff --git a/app/models/snippet_blob.rb b/app/models/snippet_blob.rb new file mode 100644 index 00000000000..d6cab74eb1a --- /dev/null +++ b/app/models/snippet_blob.rb @@ -0,0 +1,59 @@ +class SnippetBlob + include Linguist::BlobHelper + + attr_reader :snippet + + def initialize(snippet) + @snippet = snippet + end + + delegate :id, to: :snippet + + def name + snippet.file_name + end + + alias_method :path, :name + + def size + data.bytesize + end + + def data + snippet.content + end + + def rendered_markup + return unless Gitlab::MarkupHelper.gitlab_markdown?(name) + + Banzai.render_field(snippet, :content) + end + + def mode + nil + end + + def binary? + false + end + + def load_all_data!(repository) + # No-op + end + + def lfs_pointer? + false + end + + def lfs_oid + nil + end + + def lfs_size + nil + end + + def truncated? + false + end +end diff --git a/app/models/todo.rb b/app/models/todo.rb index da3fa7277c2..b011001b235 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -84,6 +84,10 @@ class Todo < ActiveRecord::Base action == BUILD_FAILED end + def assigned? + action == ASSIGNED + end + def action_name ACTION_NAMES[action] end @@ -117,6 +121,14 @@ class Todo < ActiveRecord::Base end end + def self_added? + author == user + end + + def self_assigned? + assigned? && self_added? + end + private def keep_around_commit diff --git a/app/models/user.rb b/app/models/user.rb index 2d85bf8df26..2b7ebe6c1a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,9 +99,6 @@ class User < ActiveRecord::Base has_many :award_emoji, dependent: :destroy has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id - has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" - has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" - # Issues that a user owns are expected to be moved to the "ghost" user before # the user is destroyed. If the user owns any issues during deletion, this # should be treated as an exceptional condition. @@ -121,7 +118,7 @@ class User < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } validates :username, - namespace: true, + dynamic_path: true, presence: true, uniqueness: { case_sensitive: false } @@ -891,20 +888,20 @@ class User < ActiveRecord::Base @global_notification_setting end - def assigned_open_merge_request_count(force: false) - Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do - assigned_merge_requests.opened.count + def assigned_open_merge_requests_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do + MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end def assigned_open_issues_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do - assigned_issues.opened.count + IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count end end def update_cache_counts - assigned_open_merge_request_count(force: true) + assigned_open_merge_requests_count(force: true) assigned_open_issues_count(force: true) end @@ -1071,11 +1068,13 @@ class User < ActiveRecord::Base User.find_by_email(s) end - scope.create( + user = scope.build( username: username, email: email, &creation_block ) + user.save(validate: false) + user ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end |