Commit a17412f0 authored by Robert Speicher's avatar Robert Speicher

Merge branch '18337-cache-html-in-database' into 'master'

Cache rendered Markdown fields in the database

## What does this MR do?

Introduces cache fields for Markdown-containing fields in the database, and populates them.

## Why was this MR needed?

Rendering Markdown into HTML is performance-intensive. A Redis cache already exists, but this approach is expected to be more performant and reduce unnecessary cache invalidations.

## What are the relevant issue numbers?

Closes #18337

See merge request !6095
parents c2cf1dd6 110e15da
...@@ -15,6 +15,7 @@ v 8.13.0 (unreleased) ...@@ -15,6 +15,7 @@ v 8.13.0 (unreleased)
- Keep refs for each deployment - Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods - Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
......
...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0' ...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......
...@@ -745,6 +745,9 @@ GEM ...@@ -745,6 +745,9 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
...@@ -971,6 +974,7 @@ DEPENDENCIES ...@@ -971,6 +974,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
......
...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end end
def preview def preview
@message = broadcast_message_params[:message] @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end end
protected protected
......
...@@ -16,7 +16,7 @@ module AppearancesHelper ...@@ -16,7 +16,7 @@ module AppearancesHelper
end end
def brand_text def brand_text
markdown(brand_item.description) markdown_field(brand_item, :description)
end end
def brand_item def brand_item
......
...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper ...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled? current_application_settings.signin_enabled?
end end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications? def user_oauth_applications?
current_application_settings.user_oauth_applications current_application_settings.user_oauth_applications
end end
......
...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper ...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message.message) icon('bullhorn') << ' ' << render_broadcast_message(message)
end end
end end
...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper ...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end end
end end
def render_broadcast_message(message) def render_broadcast_message(broadcast_message)
Banzai.render(message, pipeline: :broadcast_message).html_safe Banzai.render_field(broadcast_message, :message).html_safe
end end
end end
...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper ...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return "" if body.blank?
escaped_body = if body.start_with?('<img') context = {
body project: @project,
else current_user: (current_user if defined?(current_user)),
escape_once(body) pipeline: :single_line,
end }
gfm_body = Banzai.render(body, context)
user = current_user if defined?(current_user)
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper ...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project context[:project] ||= @project
html = Banzai.render(text, context) html = Banzai.render(text, context)
banzai_postprocess(html, context)
end
context.merge!( def markdown_field(object, field)
current_user: (current_user if defined?(current_user)), object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def asciidoc(text)
...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper ...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon]) icon(options[:icon])
end end
end end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context)
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context)
end
end end
...@@ -153,8 +153,18 @@ module SearchHelper ...@@ -153,8 +153,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
# Sanitize html generated after parsing markdown from issue description or comment # Sanitize a HTML field for search display. Most tags are stripped out and the
def search_md_sanitize(html) # maximum length is set to 200 characters.
def search_md_sanitize(object, field)
html = markdown_field(object, field)
html = Truncato.truncate(
html,
count_tags: false,
count_tail: false,
max_length: 200
)
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code)) sanitize(html, tags: %w(a p ol ul li pre code))
end end
end end
class AbuseReport < ActiveRecord::Base class AbuseReport < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :message, pipeline: :single_line
belongs_to :reporter, class_name: 'User' belongs_to :reporter, class_name: 'User'
belongs_to :user belongs_to :user
...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base ...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' } validates :user_id, uniqueness: { message: 'has already been reported' }
# For CacheMarkdownField
alias_method :author, :reporter
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
......
class Appearance < ActiveRecord::Base class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
validates :title, presence: true validates :title, presence: true
validates :description, presence: true validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte } validates :logo, file_size: { maximum: 1.megabyte }
......
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array serialize :domain_whitelist, Array
serialize :domain_blacklist, Array serialize :domain_blacklist, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
cache_markdown_field :after_sign_up_text
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay, validates :session_expire_delay,
......
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include CacheMarkdownField
include Sortable include Sortable
cache_markdown_field :message, pipeline: :broadcast_message
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_at, presence: true validates :ends_at, presence: true
......
# This module takes care of updating cache columns for Markdown-containing
# fields. Use like this in the body of your class:
#
# include CacheMarkdownField
# cache_markdown_field :foo
# cache_markdown_field :bar
# cache_markdown_field :baz, pipeline: :single_line
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
extend Forwardable
def initialize
@data = {}
end
def_delegators :@data, :[], :[]=
def_delegator :@data, :keys, :markdown_fields
def html_field(markdown_field)
"#{markdown_field}_html"
end
def html_fields
markdown_fields.map {|field| html_field(field) }
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
]
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
extend ActiveSupport::Concern
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# 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)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
context
end
# 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)
cached_markdown_fields.html_field(field)
end
# Always exclude _html fields from attributes (including serialization).
# They contain unredacted HTML, which would be a security issue
alias_method :attributes_before_markdown_cache, :attributes
def attributes
attrs = attributes_before_markdown_cache
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
end
class_methods do
private
# Specify that a field is markdown. Its rendered output will be cached in
# 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
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
__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?
end
before_save cache_method, if: invalidation_method
end
end
end
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
include Subscribable include Subscribable
...@@ -13,6 +14,9 @@ module Issuable ...@@ -13,6 +14,9 @@ module Issuable
include Awardable include Awardable
included do included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
......
...@@ -4,6 +4,10 @@ class GlobalLabel ...@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label delegate :color, :description, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels) def self.build_collection(labels)
labels = labels.group_by(&:title) labels = labels.group_by(&:title)
......
...@@ -4,6 +4,10 @@ class GlobalMilestone ...@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
def for_display
@first_milestone
end
def self.build_collection(milestones) def self.build_collection(milestones)
milestones = milestones.group_by(&:title) milestones = milestones.group_by(&:title)
...@@ -17,6 +21,7 @@ class GlobalMilestone ...@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title @title = title
@name = title @name = title
@milestones = milestones @milestones = milestones
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end end
def safe_title def safe_title
......
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include CacheMarkdownField
include Referable include Referable
include Subscribable include Subscribable
...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base ...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label') None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '') Any = LabelStruct.new('Any Label', '')
cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = '#428BCA' DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
......
...@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base ...@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include CacheMarkdownField
include InternalId include InternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
include Milestoneish include Milestoneish
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project belongs_to :project
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
......
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
include CacheMarkdownField
include Sortable include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
......
...@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base ...@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable include Awardable
include Importable include Importable
include FasterCacheKeys include FasterCacheKeys
include CacheMarkdownField
cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by # Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer. # Banzai::ObjectRenderer.
attr_accessor :note_html attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by # An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer # Banzai::ObjectRenderer
......
...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include AccessRequestable include AccessRequestable
include CacheMarkdownField
include Referable include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
...@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base ...@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
UNKNOWN_IMPORT_URL = 'http://unknown.git' UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false default_value_for :archived, false
......
class Release < ActiveRecord::Base class Release < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
belongs_to :project belongs_to :project
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
......
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Linguist::BlobHelper include Linguist::BlobHelper
include CacheMarkdownField
include Participable include Participable
include Referable include Referable
include Sortable include Sortable
include Awardable include Awardable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
# If file_name changes, it invalidates content
alias_method :default_content_html_invalidator, :content_html_invalidated?
def content_html_invalidated?
default_content_html_invalidator || file_name_changed?
end
default_value_for :visibility_level, Snippet::PRIVATE default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Message %strong.subheading.visible-xs-block.visible-sm-block Message
.message .message
= markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) = markdown_field(abuse_report, :message)
%td %td
- if user - if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
......
.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
= icon('bullhorn') = icon('bullhorn')
.js-broadcast-message-preview .js-broadcast-message-preview
= render_broadcast_message(@broadcast_message.message.presence || "Your message here") - if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
= "Your message here"
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message) = form_errors(@broadcast_message)
......
$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); $('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
...@@ -23,4 +23,4 @@ ...@@ -23,4 +23,4 @@
- if group.description.present? - if group.description.present?
.description .description
= markdown(group.description, pipeline: :description) = markdown_field(group, :description)
%li{id: dom_id(label)} %li{id: dom_id(label)}
.label-row .label-row
= render_colored_label(label, tooltip: false) = render_colored_label(label, tooltip: false)
= markdown(label.description, pipeline: :single_line) = markdown_field(label, :description)
.pull-right .pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
- if project.description.present? - if project.description.present?
.description .description
= markdown(project.description, pipeline: :description) = markdown_field(project, :description)
= paginate @projects, theme: 'gitlab' = paginate @projects, theme: 'gitlab'
- else - else
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
Almost there... Almost there...
%p.lead %p.lead
Please check your email to confirm your account Please check your email to confirm your account
- if after_sign_up_text.present? - if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center .well-confirmation.text-center
= markdown(after_sign_up_text) = markdown_field(current_application_settings, :after_sign_up_text)
%p.confirmation-content.text-center %p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center .append-bottom-20.prepend-top-20.text-center
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
- if @group.description.present? - if @group.description.present?
.cover-desc.description .cover-desc.description
= markdown(@group.description, pipeline: :description) = markdown_field(@group, :description)
%div.groups-header{ class: container_class } %div.groups-header{ class: container_class }
.top-area .top-area
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
- if current_application_settings.help_page_text.present? - if current_application_settings.help_page_text.present?
%hr %hr
= markdown(current_application_settings.help_page_text) = markdown_field(current_application_settings, :help_page_text)
%hr %hr
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
Perform code reviews and enhance collaboration with merge requests. Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki. Each project can also have an issue tracker and a wiki.
- if extra_sign_in_text.present? - if current_application_settings.sign_in_text.present?
= markdown(extra_sign_in_text) = markdown_field(current_application_settings, :sign_in_text)
%hr %hr
.container .container
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.project-home-desc .project-home-desc
- if @project.description.present? - if @project.description.present?
= markdown(@project.description, pipeline: :description) = markdown_field(@project, :description)
- if forked_from_project = @project.forked_from_project - if forked_from_project = @project.forked_from_project
%p %p
......
...@@ -65,10 +65,10 @@ ...@@ -65,10 +65,10 @@
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present? - if @commit.description.present?
%pre.commit-description %pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
:javascript :javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
- if commit.description? - if commit.description?
%pre.commit-row-description.js-toggle-content %pre.commit-row-description.js-toggle-content
= preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commit-row-info .commit-row-info
= commit_author_link(commit, avatar: false, size: 24) = commit_author_link(commit, avatar: false, size: 24)
......
...@@ -55,12 +55,12 @@ ...@@ -55,12 +55,12 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author = markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki .wiki
= preserve do = preserve do
= markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @issue.description = @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
......
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author = markdown_field(@merge_request, :title)
%div %div
- if @merge_request.description.present? - if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki .wiki
= preserve do = preserve do
= markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) = markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @merge_request.description = @merge_request.description
......
...@@ -30,13 +30,13 @@ ...@@ -30,13 +30,13 @@
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
%h2.title %h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line = markdown_field(@milestone, :title)
%div %div
- if @milestone.description.present? - if @milestone.description.present?
.description .description
.wiki .wiki
= preserve do = preserve do
= markdown @milestone.description = markdown_field(@milestone, :description)
- if @milestone.total_items_count(current_user).zero? - if @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
.note-body{class: note_editable ? 'js-task-list-container' : ''} .note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text.md .note-text.md
= preserve do = preserve do
= note.note_html = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
- if @commit - if @commit
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line = markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present? - if @commit.description.present?
%pre.commit-description %pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) = preserve(markdown(@commit.description, pipeline: :single_line))
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id %code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
= markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td %td
%span.pull-right.cgray %span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date) = time_ago_with_tooltip(commit.committed_date)
%h3 Shared Runners %h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description .bs-callout.bs-callout-warning.shared-runners-description
- if shared_runners_text.present? - if current_application_settings.shared_runners_text.present?
= markdown(shared_runners_text, pipeline: 'plain_markdown') = markdown_field(current_application_settings, :shared_runners_text)
- else - else
GitLab Shared Runners execute code of different projects on the same Runner GitLab Shared Runners execute code of different projects on the same Runner
unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
......
...@@ -30,4 +30,4 @@ ...@@ -30,4 +30,4 @@
.description.prepend-top-default .description.prepend-top-default
.wiki .wiki
= preserve do = preserve do
= markdown release.description = markdown_field(release, :description)
...@@ -33,6 +33,6 @@ ...@@ -33,6 +33,6 @@
.description .description
.wiki .wiki
= preserve do = preserve do
= markdown @release.description = markdown_field(@release, :description)
- else - else
This tag has no release notes. This tag has no release notes.
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if issue.description.present? - if issue.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author })) = search_md_sanitize(issue, :description)
%span.light %span.light
#{issue.project.name_with_namespace} #{issue.project.name_with_namespace}
- if issue.closed? - if issue.closed?
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if merge_request.description.present? - if merge_request.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author })) = search_md_sanitize(merge_request, :description)
%span.light %span.light
#{merge_request.project.name_with_namespace} #{merge_request.project.name_with_namespace}
.pull-right .pull-right
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
- if milestone.description.present? - if milestone.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(milestone.description)) = search_md_sanitize(milestone, :description)
...@@ -23,4 +23,4 @@ ...@@ -23,4 +23,4 @@
.note-search-result .note-search-result
.term .term
= preserve do = preserve do
= search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author})) = search_md_sanitize(note, :note)
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
= link_to_label(label, tooltip: false) = link_to_label(label, tooltip: false)
- if label.description - if label.description
%span.label-description %span.label-description
= markdown(label.description, pipeline: :single_line) = markdown_field(label, :description)
...@@ -35,4 +35,4 @@ ...@@ -35,4 +35,4 @@
- if group.description.present? - if group.description.present?
.description .description
= markdown(group.description, pipeline: :description) = markdown_field(group, :description)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= link_to milestones_label_path(options) do = link_to milestones_label_path(options) do
- render_colored_label(label, tooltip: false) - render_colored_label(label, tooltip: false)
%span.prepend-description-left %span.prepend-description-left
= markdown(label.description, pipeline: :single_line) = markdown_field(label, :description)
.pull-info-right .pull-info-right
%span.append-right-20 %span.append-right-20
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
%h2.title %h2.title
= markdown escape_once(milestone.title), pipeline: :single_line = markdown_field(milestone, :title)
- if milestone.complete?(current_user) && milestone.active? - if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
...@@ -55,4 +55,3 @@ ...@@ -55,4 +55,3 @@
Open Open
%td %td
= ms.expires_at = ms.expires_at
...@@ -50,4 +50,4 @@ ...@@ -50,4 +50,4 @@
class: "commit-row-message" class: "commit-row-message"
- elsif project.description.present? - elsif project.description.present?
.description .description
= markdown(project.description, pipeline: :description) = markdown_field(project, :description)
- unless @snippet.content.empty? - unless @snippet.content.empty?
- if markup?(@snippet.file_name) - if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
= @snippet.data = @snippet.content
.file-content.wiki .file-content.wiki
= render_markup(@snippet.file_name, @snippet.data) - if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content))
- else
= render_markup(@snippet.file_name, @snippet.content)
- else - else
= render 'shared/file_highlight', blob: @snippet = render 'shared/file_highlight', blob: @snippet
- else - else
......
...@@ -21,4 +21,4 @@ ...@@ -21,4 +21,4 @@
= render "snippets/actions" = render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0 %h2.snippet-title.prepend-top-0.append-bottom-0
= markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author = markdown_field(@snippet, :title)
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
BATCH_SIZE = 1000
def perform
CacheMarkdownField.caching_classes.each do |kls|
fields = kls.cached_markdown_fields.html_fields
clear_cache_fields = fields.each_with_object({}) do |field, memo|
memo[field] = nil
end
Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
relation.update_all(clear_cache_fields)
end
end
nil
end
end
# Port ActiveRecord::Relation#in_batches from ActiveRecord 5.
# https://github.com/rails/rails/blob/ac027338e4a165273607dccee49a3d38bc836794/activerecord/lib/active_record/relation/batches.rb#L184
# TODO: this can be removed once we're using AR5.
raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
module ActiveRecord
module Batches
# Differences from upstream: enumerator support was removed, and custom
# order/limit clauses are ignored without a warning.
def in_batches(of: 1000, start: nil, finish: nil, load: false)
raise "Must provide a block" unless block_given?
relation = self.reorder(batch_order).limit(of)
relation = relation.where(arel_table[primary_key].gteq(start)) if start
relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
batch_relation = relation
loop do
if load
records = batch_relation.records
ids = records.map(&:id)
yielded_relation = self.where(primary_key => ids)
yielded_relation.load_records(records)
else
ids = batch_relation.pluck(primary_key)
yielded_relation = self.where(primary_key => ids)
end
break if ids.empty?
primary_key_offset = ids.last
raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
yield yielded_relation
break if ids.length < of
batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
end
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddMarkdownCacheColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
COLUMNS = {
abuse_reports: [:message],
appearances: [:description],
application_settings: [
:sign_in_text,
:help_page_text,
:shared_runners_text,
:after_sign_up_text
],
broadcast_messages: [:message],
issues: [:title, :description],
labels: [:description],
merge_requests: [:title, :description],
milestones: [:title, :description],
namespaces: [:description],
notes: [:note],
projects: [:description],
releases: [:description],
snippets: [:title, :content],
}
def change
COLUMNS.each do |table, columns|
columns.each do |column|
add_column table, "#{column}_html", :text
end
end
end
end
...@@ -23,6 +23,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -23,6 +23,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "message" t.text "message"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.text "message_html"
end end
create_table "appearances", force: :cascade do |t| create_table "appearances", force: :cascade do |t|
...@@ -30,8 +31,9 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -30,8 +31,9 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "description" t.text "description"
t.string "header_logo" t.string "header_logo"
t.string "logo" t.string "logo"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "description_html"
end end
create_table "application_settings", force: :cascade do |t| create_table "application_settings", force: :cascade do |t|
...@@ -92,6 +94,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -92,6 +94,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "domain_blacklist" t.text "domain_blacklist"
t.boolean "koding_enabled" t.boolean "koding_enabled"
t.string "koding_url" t.string "koding_url"
t.text "sign_in_text_html"
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
...@@ -128,13 +134,14 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -128,13 +134,14 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t| create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false t.text "message", null: false
t.datetime "starts_at" t.datetime "starts_at"
t.datetime "ends_at" t.datetime "ends_at"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "color" t.string "color"
t.string "font" t.string "font"
t.text "message_html"
end end
create_table "ci_application_settings", force: :cascade do |t| create_table "ci_application_settings", force: :cascade do |t|
...@@ -457,18 +464,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -457,18 +464,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "position", default: 0 t.integer "position", default: 0
t.string "branch_name" t.string "branch_name"
t.text "description" t.text "description"
t.integer "milestone_id" t.integer "milestone_id"
t.string "state" t.string "state"
t.integer "iid" t.integer "iid"
t.integer "updated_by_id" t.integer "updated_by_id"
t.boolean "confidential", default: false t.boolean "confidential", default: false
t.datetime "deleted_at" t.datetime "deleted_at"
t.date "due_date" t.date "due_date"
t.integer "moved_to_id" t.integer "moved_to_id"
t.integer "lock_version" t.integer "lock_version"
t.text "title_html"
t.text "description_html"
end end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
...@@ -514,9 +523,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -514,9 +523,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "template", default: false t.boolean "template", default: false
t.string "description" t.string "description"
t.integer "priority" t.integer "priority"
t.text "description_html"
end end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
...@@ -632,6 +642,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -632,6 +642,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.datetime "deleted_at" t.datetime "deleted_at"
t.string "in_progress_merge_commit_sha" t.string "in_progress_merge_commit_sha"
t.integer "lock_version" t.integer "lock_version"
t.text "title_html"
t.text "description_html"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
...@@ -658,14 +670,16 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -658,14 +670,16 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
create_table "milestones", force: :cascade do |t| create_table "milestones", force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
t.text "description" t.text "description"
t.date "due_date" t.date "due_date"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "state" t.string "state"
t.integer "iid" t.integer "iid"
t.text "title_html"
t.text "description_html"
end end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
...@@ -689,6 +703,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -689,6 +703,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.boolean "request_access_enabled", default: true, null: false t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at" t.datetime "deleted_at"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.text "description_html"
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -721,6 +736,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -721,6 +736,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "resolved_by_id" t.integer "resolved_by_id"
t.string "discussion_id" t.string "discussion_id"
t.string "original_discussion_id" t.string "original_discussion_id"
t.text "note_html"
end end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
...@@ -872,6 +888,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -872,6 +888,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.boolean "request_access_enabled", default: true, null: false t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki" t.boolean "has_external_wiki"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.text "description_html"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
...@@ -922,6 +939,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -922,6 +939,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.text "description_html"
end end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
...@@ -976,6 +994,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do ...@@ -976,6 +994,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.string "file_name" t.string "file_name"
t.string "type" t.string "type"
t.integer "visibility_level", default: 0, null: false t.integer "visibility_level", default: 0, null: false
t.text "title_html"
t.text "content_html"
end end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
......
...@@ -3,6 +3,10 @@ module Banzai ...@@ -3,6 +3,10 @@ module Banzai
Renderer.render(text, context) Renderer.render(text, context)
end end
def self.render_field(object, field)
Renderer.render_field(object, field)
end
def self.cache_collection_render(texts_and_contexts) def self.cache_collection_render(texts_and_contexts)
Renderer.cache_collection_render(texts_and_contexts) Renderer.cache_collection_render(texts_and_contexts)
end end
......
require 'erb'
module Banzai
module Filter
# Text filter that escapes these HTML entities: & " < >
class HTMLEntityFilter < HTML::Pipeline::TextFilter
def call
ERB::Util.html_escape(text)
end
end
end
end
...@@ -3,7 +3,7 @@ module Banzai ...@@ -3,7 +3,7 @@ module Banzai
# Renders a collection of Note instances. # Renders a collection of Note instances.
# #
# notes - The notes to render. # notes - The notes to render.
# project - The project to use for rendering/redacting. # project - The project to use for redacting.
# user - The user viewing the notes. # user - The user viewing the notes.
# path - The request path. # path - The request path.
# wiki - The project's wiki. # wiki - The project's wiki.
...@@ -13,8 +13,7 @@ module Banzai ...@@ -13,8 +13,7 @@ module Banzai
user, user,
requested_path: path, requested_path: path,
project_wiki: wiki, project_wiki: wiki,
ref: git_ref, ref: git_ref)
pipeline: :note)
renderer.render(notes, :note) renderer.render(notes, :note)
end end
......
module Banzai module Banzai
# Class for rendering multiple objects (e.g. Note instances) in a single pass. # Class for rendering multiple objects (e.g. Note instances) in a single pass,
# using +render_field+ to benefit from caching in the database. Rendering and
# redaction are both performed.
# #
# Rendered Markdown is stored in an attribute in every object based on the # The unredacted HTML is generated according to the usual +render_field+
# name of the attribute containing the Markdown. For example, when the # policy, so specify the pipeline and any other context options on the model.
# attribute `note` is rendered the HTML is stored in `note_html`. #
# The *redacted* (i.e., suitable for use) HTML is placed in an attribute
# named "redacted_<foo>", where <foo> is the name of the cache field for the
# chosen attribute.
#
# As an example, rendering the attribute `note` would place the unredacted
# HTML into `note_html` and the redacted HTML into `redacted_note_html`.
class ObjectRenderer class ObjectRenderer
attr_reader :project, :user attr_reader :project, :user
# Make sure to set the appropriate pipeline in the `raw_context` attribute # project - A Project to use for redacting Markdown.
# (e.g. `:note` for Note instances).
#
# project - A Project to use for rendering and redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any. # user - The user viewing the Markdown/HTML documents, if any.
# context - A Hash containing extra attributes to use in the rendering # context - A Hash containing extra attributes to use during redaction
# pipeline. def initialize(project, user = nil, redaction_context = {})
def initialize(project, user = nil, raw_context = {})
@project = project @project = project
@user = user @user = user
@raw_context = raw_context @redaction_context = redaction_context
end end
# Renders and redacts an Array of objects. # Renders and redacts an Array of objects.
# #
# objects - The objects to render # objects - The objects to render.
# attribute - The attribute containing the raw Markdown to render. # attribute - The attribute containing the raw Markdown to render.
# #
# Returns the same input objects. # Returns the same input objects.
...@@ -32,7 +36,7 @@ module Banzai ...@@ -32,7 +36,7 @@ module Banzai
objects.each_with_index do |object, index| objects.each_with_index do |object, index|
redacted_data = redacted[index] redacted_data = redacted[index]
object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe) object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe)
object.user_visible_reference_count = redacted_data[:visible_reference_count] object.user_visible_reference_count = redacted_data[:visible_reference_count]
end end
end end
...@@ -53,12 +57,8 @@ module Banzai ...@@ -53,12 +57,8 @@ module Banzai
# Returns a Banzai context for the given object and attribute. # Returns a Banzai context for the given object and attribute.
def context_for(object, attribute) def context_for(object, attribute)
context = base_context.merge(cache_key: [object, attribute]) context = base_context.dup
context = context.merge(object.banzai_render_context(attribute))
if object.respond_to?(:author)
context[:author] = object.author
end
context context
end end
...@@ -66,21 +66,16 @@ module Banzai ...@@ -66,21 +66,16 @@ module Banzai
# #
# Returns an Array of `Nokogiri::HTML::Document`. # Returns an Array of `Nokogiri::HTML::Document`.
def render_attributes(objects, attribute) def render_attributes(objects, attribute)
strings_and_contexts = objects.map do |object| objects.map do |object|
string = Banzai.render_field(object, attribute)
context = context_for(object, attribute) context = context_for(object, attribute)
string = object.__send__(attribute) Banzai::Pipeline[:relative_link].to_document(string, context)
{ text: string, context: context }
end
Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index|
Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context])
end end
end end
def base_context def base_context
@base_context ||= @raw_context.merge(current_user: user, project: project) @base_context ||= @redaction_context.merge(current_user: user, project: project)
end end
end end
end end
...@@ -3,6 +3,7 @@ module Banzai ...@@ -3,6 +3,7 @@ module Banzai
class SingleLinePipeline < GfmPipeline class SingleLinePipeline < GfmPipeline
def self.filters def self.filters
@filters ||= FilterArray[ @filters ||= FilterArray[
Filter::HTMLEntityFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
......
...@@ -31,6 +31,34 @@ module Banzai ...@@ -31,6 +31,34 @@ module Banzai
end end
end end
# Convert a Markdown-containing field on an object into an HTML-safe String
# of HTML. This method is analogous to calling render(object.field), but it
# can cache the rendered HTML in the object, rather than Redis.
#
# The context to use is learned from the passed-in object by calling
# #banzai_render_context(field), and cannot be changed. Use #render, passing
# it the field text, if a custom rendering is needed. The generated context
# is returned along with the HTML.
def render_field(object, field)
html_field = object.markdown_cache_field_for(field)
html = object.__send__(html_field)
return html if html.present?
html = cacheless_render_field(object, field)
object.update_column(html_field, html) unless object.new_record? || object.destroyed?
html
end
# Same as +render_field+, but without consulting or updating the cache field
def cacheless_render_field(object, field)
text = object.__send__(field)
context = object.banzai_render_context(field)
cacheless_render(text, context)
end
# Perform multiple render from an Array of Markdown String into an # Perform multiple render from an Array of Markdown String into an
# Array of HTML-safe String of HTML. # Array of HTML-safe String of HTML.
# #
......
namespace :cache do namespace :cache do
CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000 namespace :clear do
REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
desc "GitLab | Clear redis cache" desc "GitLab | Clear redis cache"
task :clear => :environment do task redis: :environment do
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
cursor = REDIS_SCAN_START_STOP cursor = REDIS_SCAN_START_STOP
loop do loop do
cursor, keys = redis.scan( cursor, keys = redis.scan(
cursor, cursor,
match: "#{Gitlab::Redis::CACHE_NAMESPACE}*", match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
count: CLEAR_BATCH_SIZE count: REDIS_CLEAR_BATCH_SIZE
) )
redis.del(*keys) if keys.any? redis.del(*keys) if keys.any?
break if cursor == REDIS_SCAN_START_STOP break if cursor == REDIS_SCAN_START_STOP
end
end end
end end
desc "GitLab | Clear database cache (in the background)"
task db: :environment do
ClearDatabaseCacheWorker.perform_async
end
task all: [:db, :redis]
end end
task clear: 'cache:clear:all'
end end
...@@ -9,6 +9,9 @@ FactoryGirl.define do ...@@ -9,6 +9,9 @@ FactoryGirl.define do
namespace namespace
creator creator
# Behaves differently to nil due to cache_has_external_issue_tracker
has_external_issue_tracker false
trait :public do trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC visibility_level Gitlab::VisibilityLevel::PUBLIC
end end
...@@ -92,6 +95,8 @@ FactoryGirl.define do ...@@ -92,6 +95,8 @@ FactoryGirl.define do
end end
factory :redmine_project, parent: :project do factory :redmine_project, parent: :project do
has_external_issue_tracker true
after :create do |project| after :create do |project|
project.create_redmine_service( project.create_redmine_service(
active: true, active: true,
...@@ -105,6 +110,8 @@ FactoryGirl.define do ...@@ -105,6 +110,8 @@ FactoryGirl.define do
end end
factory :jira_project, parent: :project do factory :jira_project, parent: :project do
has_external_issue_tracker true
after :create do |project| after :create do |project|
project.create_jira_service( project.create_jira_service(
active: true, active: true,
......
...@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do ...@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do
end end
it 'includes the current message' do it 'includes the current message' do
current = double(message: 'Current Message') current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return(nil) allow(helper).to receive(:broadcast_message_style).and_return(nil)
...@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do ...@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do
end end
it 'includes custom style' do it 'includes custom style' do
current = double(message: 'Current Message') current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return('foo') allow(helper).to receive(:broadcast_message_style).and_return('foo')
......
require 'spec_helper'
describe Banzai::Filter::HTMLEntityFilter, lib: true do
include FilterSpecHelper
let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' }
let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' }
it 'converts common entities to their HTML-escaped equivalents' do
output = filter(unescaped)
expect(output).to eq(escaped)
end
end
...@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do ...@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do
with(project, user, with(project, user,
requested_path: 'foo', requested_path: 'foo',
project_wiki: wiki, project_wiki: wiki,
ref: 'bar', ref: 'bar').
pipeline: :note).
and_call_original and_call_original
expect_any_instance_of(Banzai::ObjectRenderer). expect_any_instance_of(Banzai::ObjectRenderer).
......
...@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do ...@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { project.owner } let(:user) { project.owner }
def fake_object(attrs = {})
object = double(attrs.merge("new_record?": true, "destroyed?": true))
allow(object).to receive(:markdown_cache_field_for).with(:note).and_return(:note_html)
allow(object).to receive(:banzai_render_context).with(:note).and_return(project: nil, author: nil)
allow(object).to receive(:update_column).with(:note_html, anything).and_return(true)
object
end
describe '#render' do describe '#render' do
it 'renders and redacts an Array of objects' do it 'renders and redacts an Array of objects' do
renderer = described_class.new(project, user) renderer = described_class.new(project, user)
object = double(:object, note: 'hello', note_html: nil) object = fake_object(note: 'hello', note_html: nil)
expect(renderer).to receive(:render_objects).with([object], :note). expect(renderer).to receive(:render_objects).with([object], :note).
and_call_original and_call_original
...@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do ...@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)). with(an_instance_of(Array)).
and_call_original and_call_original
expect(object).to receive(:note_html=).with('<p>hello</p>') expect(object).to receive(:redacted_note_html=).with('<p>hello</p>')
expect(object).to receive(:user_visible_reference_count=).with(0) expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note) renderer.render([object], :note)
...@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do ...@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do
describe '#render_objects' do describe '#render_objects' do
it 'renders an Array of objects' do it 'renders an Array of objects' do
object = double(:object, note: 'hello') object = fake_object(note: 'hello', note_html: nil)
renderer = described_class.new(project, user) renderer = described_class.new(project, user)
...@@ -57,49 +65,29 @@ describe Banzai::ObjectRenderer do ...@@ -57,49 +65,29 @@ describe Banzai::ObjectRenderer do
end end
describe '#context_for' do describe '#context_for' do
let(:object) { double(:object, note: 'hello') } let(:object) { fake_object(note: 'hello') }
let(:renderer) { described_class.new(project, user) } let(:renderer) { described_class.new(project, user) }
it 'returns a Hash' do it 'returns a Hash' do
expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash) expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
end end
it 'includes the cache key' do it 'includes the banzai render context for the object' do
expect(object).to receive(:banzai_render_context).with(:note).and_return(foo: :bar)
context = renderer.context_for(object, :note) context = renderer.context_for(object, :note)
expect(context).to have_key(:foo)
expect(context[:cache_key]).to eq([object, :note]) expect(context[:foo]).to eq(:bar)
end
context 'when the object responds to "author"' do
it 'includes the author in the context' do
expect(object).to receive(:author).and_return('Alice')
context = renderer.context_for(object, :note)
expect(context[:author]).to eq('Alice')
end
end
context 'when the object does not respond to "author"' do
it 'does not include the author in the context' do
context = renderer.context_for(object, :note)
expect(context.key?(:author)).to eq(false)
end
end end
end end
describe '#render_attributes' do describe '#render_attributes' do
it 'renders the attribute of a list of objects' do it 'renders the attribute of a list of objects' do
objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')] objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)]
renderer = described_class.new(project, user, pipeline: :note) renderer = described_class.new(project, user)
expect(Banzai).to receive(:cache_collection_render). objects.each do |object|
with([ expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
{ text: 'hello', context: renderer.context_for(objects[0], :note) }, end
{ text: 'bye', context: renderer.context_for(objects[1], :note) }
]).
and_call_original
docs = renderer.render_attributes(objects, :note) docs = renderer.render_attributes(objects, :note)
...@@ -114,17 +102,13 @@ describe Banzai::ObjectRenderer do ...@@ -114,17 +102,13 @@ describe Banzai::ObjectRenderer do
objects = [] objects = []
renderer = described_class.new(project, user, pipeline: :note) renderer = described_class.new(project, user, pipeline: :note)
expect(Banzai).to receive(:cache_collection_render).
with([]).
and_call_original
expect(renderer.render_attributes(objects, :note)).to eq([]) expect(renderer.render_attributes(objects, :note)).to eq([])
end end
end end
describe '#base_context' do describe '#base_context' do
let(:context) do let(:context) do
described_class.new(project, user, pipeline: :note).base_context described_class.new(project, user, foo: :bar).base_context
end end
it 'returns a Hash' do it 'returns a Hash' do
...@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do ...@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do
end end
it 'includes the custom attributes' do it 'includes the custom attributes' do
expect(context[:pipeline]).to eq(:note) expect(context[:foo]).to eq(:bar)
end end
it 'includes the current user' do it 'includes the current user' do
......
require 'spec_helper'
describe Banzai::Renderer do
def expect_render(project = :project)
expected_context = { project: project }
expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context)
end
def expect_cache_update
expect(object).to receive(:update_column).with("field_html", :html)
end
def fake_object(*features)
markdown = :markdown if features.include?(:markdown)
html = :html if features.include?(:html)
object = double(
"object",
banzai_render_context: { project: :project },
field: markdown,
field_html: html
)
allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html")
allow(object).to receive(:new_record?).and_return(features.include?(:new))
allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed))
object
end
describe "#render_field" do
let(:renderer) { Banzai::Renderer }
let(:subject) { renderer.render_field(object, :field) }
context "with an empty cache" do
let(:object) { fake_object(:markdown) }
it "caches and returns the result" do
expect_render
expect_cache_update
expect(subject).to eq(:html)
end
end
context "with a filled cache" do
let(:object) { fake_object(:markdown, :html) }
it "uses the cache" do
expect_render.never
expect_cache_update.never
should eq(:html)
end
end
context "new object" do
let(:object) { fake_object(:new, :markdown) }
it "doesn't cache the result" do
expect_render
expect_cache_update.never
expect(subject).to eq(:html)
end
end
context "destroyed object" do
let(:object) { fake_object(:destroyed, :markdown) }
it "doesn't cache the result" do
expect_render
expect_cache_update.never
expect(subject).to eq(:html)
end
end
end
end
...@@ -26,10 +26,11 @@ describe 'Import/Export attribute configuration', lib: true do ...@@ -26,10 +26,11 @@ describe 'Import/Export attribute configuration', lib: true do
it 'has no new columns' do it 'has no new columns' do
relation_names.each do |relation_name| relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name) relation_class = relation_class_for_name(relation_name)
relation_attributes = relation_class.new.attributes.keys
expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes" expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
current_attributes = parsed_attributes(relation_name, relation_class.attribute_names) current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s] safe_attributes = safe_model_attributes[relation_class.to_s]
new_attributes = current_attributes - safe_attributes new_attributes = current_attributes - safe_attributes
......
...@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do ...@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:reporter).class_name('User') } it { is_expected.to belong_to(:reporter).class_name('User') }
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
it "aliases reporter to author" do
expect(subject.author).to be(subject.reporter)
end
end end
describe 'validations' do describe 'validations' do
......
require 'spec_helper'
describe CacheMarkdownField do
CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
include ActiveModel::Model
include ActiveModel::Dirty
include ActiveModel::Serialization
class_attribute :attribute_names
self.attribute_names = []
def attributes
attribute_names.each_with_object({}) do |name, hsh|
hsh[name.to_s] = send(name)
end
end
extend ActiveModel::Callbacks
define_model_callbacks :save
include CacheMarkdownField
cache_markdown_field :foo
cache_markdown_field :baz, pipeline: :single_line
def self.add_attr(attr_name)
self.attribute_names += [attr_name]
define_attribute_methods(attr_name)
attr_reader(attr_name)
define_method("#{attr_name}=") do |val|
send("#{attr_name}_will_change!") unless val == send(attr_name)
instance_variable_set("@#{attr_name}", val)
end
end
[:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
add_attr(attr_name)
end
def initialize(*)
super
# Pretend new is load
clear_changes_information
end
def save
run_callbacks :save do
changes_applied
end
end
end
CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
let(:markdown) { "`Foo`" }
let(:html) { "<p><code>Foo</code></p>" }
let(:updated_markdown) { "`Bar`" }
let(:updated_html) { "<p><code>Bar</code></p>" }
subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
describe ".attributes" do
it "excludes cache attributes" do
expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
end
end
describe ".cache_markdown_field" do
it "refuses to allow untracked classes" do
expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
end
end
context "an unchanged markdown field" do
before do
subject.foo = subject.foo
subject.save
end
it { expect(subject.foo).to eq(markdown) }
it { expect(subject.foo_html).to eq(html) }
it { expect(subject.foo_html_changed?).not_to be_truthy }
end
context "a changed markdown field" do
before do
subject.foo = updated_markdown
subject.save
end
it { expect(subject.foo_html).to eq(updated_html) }
end
context "a non-markdown field changed" do
before do
subject.bar = "OK"
subject.save
end
it { expect(subject.bar).to eq("OK") }
it { expect(subject.foo).to eq(markdown) }
it { expect(subject.foo_html).to eq(html) }
end
describe '#banzai_render_context' do
it "sets project to nil if the object lacks a project" do
context = subject.banzai_render_context(:foo)
expect(context).to have_key(:project)
expect(context[:project]).to be_nil
end
it "excludes author if the object lacks an author" do
context = subject.banzai_render_context(:foo)
expect(context).not_to have_key(:author)
end
it "raises if the context for an unrecognised field is requested" do
expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
end
it "includes the pipeline" do
context = subject.banzai_render_context(:baz)
expect(context[:pipeline]).to eq(:single_line)
end
it "returns copies of the context template" do
template = subject.cached_markdown_fields[:baz]
copy = subject.banzai_render_context(:baz)
expect(copy).not_to be(template)
end
context "with a project" do
subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
it "sets the project in the context" do
context = subject.banzai_render_context(:foo)
expect(context).to have_key(:project)
expect(context[:project]).to eq(:project)
end
it "invalidates the cache when project changes" do
subject.project = :new_project
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
subject.save
expect(subject.foo_html).to eq(updated_html)
expect(subject.baz_html).to eq(updated_html)
end
end
context "with an author" do
subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
it "sets the author in the context" do
context = subject.banzai_render_context(:foo)
expect(context).to have_key(:author)
expect(context[:author]).to eq(:author)
end
it "invalidates the cache when author changes" do
subject.author = :new_author
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
subject.save
expect(subject.foo_html).to eq(updated_html)
expect(subject.baz_html).to eq(updated_html)
end
end
end
end
...@@ -518,7 +518,7 @@ describe Project, models: true do ...@@ -518,7 +518,7 @@ describe Project, models: true do
end end
describe '#cache_has_external_issue_tracker' do describe '#cache_has_external_issue_tracker' do
let(:project) { create(:project) } let(:project) { create(:project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new]) services = double(:service, external_issue_trackers: [RedmineService.new])
......
...@@ -238,7 +238,7 @@ describe Service, models: true do ...@@ -238,7 +238,7 @@ describe Service, models: true do
it "updates the has_external_issue_tracker boolean" do it "updates the has_external_issue_tracker boolean" do
expect do expect do
service.save! service.save!
end.to change { service.project.has_external_issue_tracker }.from(nil).to(true) end.to change { service.project.has_external_issue_tracker }.from(false).to(true)
end end
end end
......
...@@ -46,6 +46,13 @@ describe Snippet, models: true do ...@@ -46,6 +46,13 @@ describe Snippet, models: true do
end end
end end
describe "#content_html_invalidated?" do
let(:snippet) { create(:snippet, content: "md", content_html: "html", file_name: "foo.md") }
it "invalidates the HTML cache of content when the filename changes" do
expect { snippet.file_name = "foo.rb" }.to change { snippet.content_html_invalidated? }.from(false).to(true)
end
end
describe '.search' do describe '.search' do
let(:snippet) { create(:snippet) } let(:snippet) { create(:snippet) }
......
...@@ -232,7 +232,7 @@ describe API::API, api: true do ...@@ -232,7 +232,7 @@ describe API::API, api: true do
post api('/projects', user), project post api('/projects', user), project
project.each_pair do |k, v| project.each_pair do |k, v|
next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k) next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
expect(json_response[k.to_s]).to eq(v) expect(json_response[k.to_s]).to eq(v)
end end
...@@ -360,7 +360,7 @@ describe API::API, api: true do ...@@ -360,7 +360,7 @@ describe API::API, api: true do
post api("/projects/user/#{user.id}", admin), project post api("/projects/user/#{user.id}", admin), project
project.each_pair do |k, v| project.each_pair do |k, v|
next if k == :path next if %i[has_external_issue_tracker path].include?(k)
expect(json_response[k.to_s]).to eq(v) expect(json_response[k.to_s]).to eq(v)
end end
end end
......
...@@ -448,6 +448,8 @@ describe GitPushService, services: true do ...@@ -448,6 +448,8 @@ describe GitPushService, services: true do
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? } let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do before do
# project.create_jira_service doesn't seem to invalidate the cache here
project.has_external_issue_tracker = true
jira_service_settings jira_service_settings
WebMock.stub_request(:post, jira_api_transition_url) WebMock.stub_request(:post, jira_api_transition_url)
......
...@@ -60,7 +60,10 @@ describe MergeRequests::MergeService, services: true do ...@@ -60,7 +60,10 @@ describe MergeRequests::MergeService, services: true do
let(:jira_tracker) { project.create_jira_service } let(:jira_tracker) { project.create_jira_service }
before { jira_service_settings } before do
project.update_attributes!(has_external_issue_tracker: true)
jira_service_settings
end
it 'closes issues on JIRA issue tracker' do it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project) jira_issue = ExternalIssue.new('JIRA-123', project)
......
...@@ -531,12 +531,12 @@ describe SystemNoteService, services: true do ...@@ -531,12 +531,12 @@ describe SystemNoteService, services: true do
include JiraServiceHelper include JiraServiceHelper
describe 'JIRA integration' do describe 'JIRA integration' do
let(:project) { create(:project) } let(:project) { create(:jira_project) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? } let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit } let(:commit) { project.commit }
context 'in JIRA issue tracker' do context 'in JIRA issue tracker' do
...@@ -545,10 +545,6 @@ describe SystemNoteService, services: true do ...@@ -545,10 +545,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:post, jira_api_comment_url) WebMock.stub_request(:post, jira_api_comment_url)
end end
after do
jira_tracker.destroy!
end
describe "new reference" do describe "new reference" do
before do before do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
...@@ -578,10 +574,6 @@ describe SystemNoteService, services: true do ...@@ -578,10 +574,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end end
after do
jira_tracker.destroy!
end
subject { described_class.cross_reference(jira_issue, issue, author) } subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(jira_status_message) } it { is_expected.to eq(jira_status_message) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment