summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG4
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/stylesheets/pages/environments.scss33
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb12
-rw-r--r--app/helpers/avatars_helper.rb5
-rw-r--r--app/helpers/broadcast_messages_helper.rb6
-rw-r--r--app/helpers/gitlab_markdown_helper.rb44
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/search_helper.rb14
-rw-r--r--app/models/abuse_report.rb7
-rw-r--r--app/models/appearance.rb4
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/broadcast_message.rb3
-rw-r--r--app/models/concerns/cache_markdown_field.rb131
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/global_label.rb4
-rw-r--r--app/models/global_milestone.rb5
-rw-r--r--app/models/label.rb3
-rw-r--r--app/models/member.rb7
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/namespace.rb3
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/release.rb4
-rw-r--r--app/models/snippet.rb10
-rw-r--r--app/models/user.rb2
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml5
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/devise/confirmations/almost_there.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml6
-rw-r--r--app/views/projects/deployments/_deployment.html.haml10
-rw-r--r--app/views/projects/environments/_environment.html.haml15
-rw-r--r--app/views/projects/environments/index.html.haml46
-rw-r--r--app/views/projects/environments/show.html.haml43
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml4
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/workers/clear_database_cache_worker.rb23
-rw-r--r--config/initializers/ar5_batching.rb41
-rw-r--r--config/initializers/gitlab_shell_secret_token.rb2
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb38
-rw-r--r--db/schema.rb36
-rw-r--r--doc/api/projects.md455
-rw-r--r--doc/university/README.md282
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/update/8.12-to-8.13.md4
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/projects.rb51
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/html_entity_filter.rb12
-rw-r--r--lib/banzai/note_renderer.rb5
-rw-r--r--lib/banzai/object_renderer.rb53
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb1
-rw-r--r--lib/banzai/renderer.rb28
-rw-r--r--lib/gitlab/backend/shell.rb46
-rw-r--r--lib/tasks/cache.rake43
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/features/environments_spec.rb28
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb4
-rw-r--r--spec/helpers/issues_helper_spec.rb36
-rw-r--r--spec/lib/banzai/filter/html_entity_filter_spec.rb14
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb3
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb62
-rw-r--r--spec/lib/banzai/renderer_spec.rb74
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb9
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb3
-rw-r--r--spec/models/abuse_report_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb181
-rw-r--r--spec/models/project_spec.rb7
-rw-r--r--spec/models/service_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb7
-rw-r--r--spec/requests/api/projects_spec.rb34
-rw-r--r--spec/services/git_push_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb5
-rw-r--r--spec/services/system_note_service_spec.rb12
118 files changed, 1676 insertions, 547 deletions
diff --git a/CHANGELOG b/CHANGELOG
index fec8d5fba29..5e775cec6d4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ v 8.13.0 (unreleased)
- Add link from system note to compare with previous version
- Improve issue load time performance by avoiding ORDER BY in find_by call
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
+ - Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- AbstractReferenceFilter caches project_refs on RequestStore when active
@@ -16,6 +17,7 @@ v 8.13.0 (unreleased)
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- 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
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date
@@ -47,6 +49,7 @@ v 8.13.0 (unreleased)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
+ - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
@@ -70,6 +73,7 @@ v 8.13.0 (unreleased)
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+ - Retouch environments list and deployments list
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
diff --git a/Gemfile b/Gemfile
index 3e8ce8b2fc5..426b824901e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
diff --git a/Gemfile.lock b/Gemfile.lock
index 96b49faf727..b98c3acf948 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -745,6 +745,9 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ truncato (0.7.8)
+ htmlentities (~> 4.3.1)
+ nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
@@ -971,6 +974,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
timecop (~> 0.8.0)
+ truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index d01c60ee6ab..3f19e920166 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,4 +1,15 @@
+.environments-container,
+.deployments-container {
+ width: 100%;
+ overflow: auto;
+}
+
.environments {
+ .deployment-column {
+ .avatar {
+ float: none;
+ }
+ }
.commit-title {
margin: 0;
@@ -9,6 +20,7 @@
width: 12px;
}
+ .external-url,
.dropdown-new {
color: $table-text-gray;
}
@@ -21,16 +33,35 @@
}
}
+ .build-link,
.branch-name {
color: $gl-dark-link-color;
}
+
+ .deployment {
+ .build-column {
+
+ .build-link {
+ color: $gl-dark-link-color;
+ }
+
+ .avatar {
+ float: none;
+ }
+ }
+ }
}
.table.builds.environments {
- min-width: 500px;
.icon-container {
width: 20px;
text-align: center;
}
+
+ .branch-commit {
+ .commit-id {
+ margin-right: 0;
+ }
+ }
}
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 82055006ac0..762e36ee2e9 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
- @message = broadcast_message_params[:message]
+ @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end
protected
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index de13e7a1fc2..16136d02530 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -16,7 +16,7 @@ module AppearancesHelper
end
def brand_text
- markdown(brand_item.description)
+ markdown_field(brand_item, :description)
end
def brand_item
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 6de25bea654..6229384817b 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled?
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?
current_application_settings.user_oauth_applications
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index df41473543b..b7e0ff8ecd0 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
+ css_class: 'hidden-xs'
}))
end
def user_avatar(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
+ css_class = options[:css_class] || ''
+
avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size),
- class: "avatar has-tooltip hidden-xs s#{avatar_size}",
+ class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
data: { container: 'body' }
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 43a29c96bca..eb03ced67eb 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present?
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
@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end
end
- def render_broadcast_message(message)
- Banzai.render(message, pipeline: :broadcast_message).html_safe
+ def render_broadcast_message(broadcast_message)
+ Banzai.render_field(broadcast_message, :message).html_safe
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 1a259656f31..0772d848289 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
- escaped_body = if body.start_with?('<img')
- body
- else
- escape_once(body)
- end
-
- user = current_user if defined?(current_user)
- gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
+ context = {
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+ pipeline: :single_line,
+ }
+ gfm_body = Banzai.render(body, context)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project
html = Banzai.render(text, context)
+ banzai_postprocess(html, context)
+ end
- context.merge!(
- current_user: (current_user if defined?(current_user)),
-
- # RelativeLinkFilter
- requested_path: @path,
- project_wiki: @project_wiki,
- ref: @ref
- )
+ def markdown_field(object, field)
+ object = object.for_display if object.respond_to?(:for_display)
+ return "" unless object.present?
- Banzai.post_process(html, context)
+ html = Banzai.render_field(object, field)
+ banzai_postprocess(html, object.banzai_render_context(field))
end
def asciidoc(text)
@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon])
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
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 8b212b0327a..1644c346dd8 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -113,14 +113,13 @@ module IssuesHelper
end
end
- def award_user_list(awards, current_user)
+ def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
end
- # Take first 9 OR current user + first 9
current_user_name = names.delete('You')
- names = names.first(9).insert(0, current_user_name).compact
+ names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8a7446b7cc7..aba3a3f9c5d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -153,8 +153,18 @@ module SearchHelper
search_path(options)
end
- # Sanitize html generated after parsing markdown from issue description or comment
- def search_md_sanitize(html)
+ # Sanitize a HTML field for search display. Most tags are stripped out and the
+ # 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))
end
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index b01a244032d..2340453831e 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -1,4 +1,8 @@
class AbuseReport < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :message, pipeline: :single_line
+
belongs_to :reporter, class_name: 'User'
belongs_to :user
@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
+ # For CacheMarkdownField
+ alias_method :author, :reporter
+
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 4cf8dd9a8ce..e4106e1c2e9 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,4 +1,8 @@
class Appearance < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
validates :title, presence: true
validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 55d2e07de08..c99aa7772bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,5 +1,7 @@
class ApplicationSetting < ActiveRecord::Base
+ include CacheMarkdownField
include TokenAuthenticatable
+
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, 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
validates :session_expire_delay,
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 61498140f27..cb40f33932a 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -1,6 +1,9 @@
class BroadcastMessage < ActiveRecord::Base
+ include CacheMarkdownField
include Sortable
+ cache_markdown_field :message, pipeline: :broadcast_message
+
validates :message, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
new file mode 100644
index 00000000000..90bd6490a02
--- /dev/null
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -0,0 +1,131 @@
+# 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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index ff465d2c745..c4b42ad82c7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include CacheMarkdownField
include Participable
include Mentionable
include Subscribable
@@ -13,6 +14,9 @@ module Issuable
include Awardable
included do
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
diff --git a/app/models/event.rb b/app/models/event.rb
index 633019fe0af..314d5ba438f 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -335,7 +335,7 @@ class Event < ActiveRecord::Base
# update the project. Only one query should actually perform the update,
# hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id).
- where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
+ where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
update_all(last_activity_at: created_at)
end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index ddd4bad5c21..698a7bbd327 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label
+ def for_display
+ @first_label
+ end
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index bda2b5c5d5d..cde4a568577 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
+ def for_display
+ @first_milestone
+ end
+
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title
@name = title
@milestones = milestones
+ @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
def safe_title
diff --git a/app/models/label.rb b/app/models/label.rb
index a23140b7d64..e8e12e2904e 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -1,4 +1,5 @@
class Label < ActiveRecord::Base
+ include CacheMarkdownField
include Referable
include Subscribable
@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '')
+ cache_markdown_field :description, pipeline: :single_line
+
DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR
diff --git a/app/models/member.rb b/app/models/member.rb
index 38a278ea559..b89ba8ecbb8 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -103,7 +103,12 @@ class Member < ActiveRecord::Base
}
if member.request?
- ::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute
+ ::Members::ApproveAccessRequestService.new(
+ source,
+ current_user,
+ id: member.id,
+ access_level: access_level
+ ).execute
else
member.save
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 44c3cbb2c73..23aecbfa3a6 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+ include CacheMarkdownField
include InternalId
include Sortable
include Referable
include StripAttribute
include Milestoneish
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 919b3b1f095..b7f2b2bbe61 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,9 +1,12 @@
class Namespace < ActiveRecord::Base
acts_as_paranoid
+ include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ cache_markdown_field :description, pipeline: :description
+
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
diff --git a/app/models/note.rb b/app/models/note.rb
index f2656df028b..2d644b03e4d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
+ include CacheMarkdownField
+
+ cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
- attr_accessor :note_html
+ attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
diff --git a/app/models/project.rb b/app/models/project.rb
index ecd742a17d5..88e4bd14860 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
+ include CacheMarkdownField
include Referable
include Sortable
include AfterCommitQueue
@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
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
default_value_for :archived, false
diff --git a/app/models/release.rb b/app/models/release.rb
index e196b84eb18..c936899799e 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,4 +1,8 @@
class Release < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
belongs_to :project
validates :description, :project, :tag, presence: true
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 8a1730f3f36..2373b445009 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,11 +1,21 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Linguist::BlobHelper
+ include CacheMarkdownField
include Participable
include Referable
include Sortable
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
belongs_to :author, class_name: 'User'
diff --git a/app/models/user.rb b/app/models/user.rb
index 508efd85050..892ac28d5b3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -902,7 +902,7 @@ class User < ActiveRecord::Base
if domain_matches?(allowed_domains, self.email)
valid = true
else
- error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
+ error = "domain is not authorized for sign-up"
valid = false
end
end
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 56bf6194914..05f3d9a3b50 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -21,7 +21,7 @@
%td
%strong.subheading.visible-xs-block.visible-sm-block Message
.message
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ = markdown_field(abuse_report, :message)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index f952d2e9aa1..3132d157f29 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -1,7 +1,10 @@
.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
= icon('bullhorn')
.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_errors(@broadcast_message)
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
index fbc9453c72e..c72e59640d7 100644
--- a/app/views/admin/broadcast_messages/preview.js.haml
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -1 +1 @@
-$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 77a11e49e20..adfa1eaafc9 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -23,4 +23,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index f417b2e44a4..be224d66855 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,7 +1,7 @@
%li{id: dom_id(label)}
.label-row
= render_colored_label(label, tooltip: false)
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-right
= 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?"}
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 1e755785d90..339cfc613fe 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -87,7 +87,7 @@
- if project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
= paginate @projects, theme: 'gitlab'
- else
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 73c3a3dd2eb..20cd7b0179d 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,9 +3,9 @@
Almost there...
%p.lead
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
- = markdown(after_sign_up_text)
+ = markdown_field(current_application_settings, :after_sign_up_text)
%p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 31db6ee0cad..fab61f447c2 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -21,7 +21,7 @@
- if @group.description.present?
.cover-desc.description
- = markdown(@group.description, pipeline: :description)
+ = markdown_field(@group, :description)
%div.groups-header{ class: container_class }
.top-area
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 57601ae9be0..31631887317 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -20,7 +20,7 @@
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
- if current_application_settings.help_page_text.present?
%hr
- = markdown(current_application_settings.help_page_text)
+ = markdown_field(current_application_settings, :help_page_text)
%hr
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 3d28eec84ef..a9a384bd5f3 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -25,8 +25,8 @@
Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki.
- - if extra_sign_in_text.present?
- = markdown(extra_sign_in_text)
+ - if current_application_settings.sign_in_text.present?
+ = markdown_field(current_application_settings, :sign_in_text)
%hr
.container
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8ef31ca3bda..5590198a20e 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -9,7 +9,7 @@
.project-home-desc
- if @project.description.present?
- = markdown(@project.description, pipeline: :description)
+ = markdown_field(@project, :description)
- if forked_from_project = @project.forked_from_project
%p
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 9fd87f84aaa..6c82a4e5600 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -65,10 +65,10 @@
.commit-box.content-block
%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?
%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
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 389477d0927..fb48aef0559 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -33,7 +33,7 @@
- if commit.description?
%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_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 99cb4222377..22c4a75d213 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,5 +1,11 @@
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.pull-right
+
+ - external_url = deployment.environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank', class: 'btn external-url' do
+ = icon('external-link')
+
- actions = deployment.manual_actions
- if actions.present?
.inline
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index cd95841ca5a..ca0005abd0c 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -5,14 +5,16 @@
%td
= render 'projects/deployments/commit', deployment: deployment
- %td
+ %td.build-column
- if deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
- = user_avatar(user: deployment.user, size: 20)
+ = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
+ - if deployment.user
+ by
+ = user_avatar(user: deployment.user, size: 20)
%td
#{time_ago_with_tooltip(deployment.created_at)}
- %td
+ %td.hidden-xs
= render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 36a6162a5a8..251694e897c 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -4,10 +4,17 @@
%td
= link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
- %td
+ %td.deployment-column
- if last_deployment
- = user_avatar(user: last_deployment.user, size: 20)
- %strong ##{last_deployment.id}
+ %span ##{last_deployment.iid}
+ - if last_deployment.user
+ by
+ = user_avatar(user: last_deployment.user, size: 20)
+
+ %td
+ - if last_deployment && last_deployment.deployable
+ = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
+ = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
%td
- if last_deployment
@@ -20,5 +27,5 @@
- if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)}
- %td
+ %td.hidden-xs
= render 'projects/deployments/actions', deployment: last_deployment
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index b3eb5b0011a..ab801409722 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -9,25 +9,27 @@
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment
- - if @environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.builds.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Commit
- %th
- %th
- = render @environments
+ .environments-container
+ - if @environments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any environments right now.
+ %p.blank-state-text
+ Environments are places where code gets deployed, such as staging or production.
+ %br
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci/environments")
+ - if can?(current_user, :create_environment, @project)
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+ - else
+ .table-holder
+ %table.table.builds.environments
+ %tbody
+ %th Environment
+ %th Last Deployment
+ %th Build
+ %th Commit
+ %th
+ %th.hidden-xs
+ = render @environments
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 8f8c1c4ce22..7a8d196cf4e 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -12,26 +12,27 @@
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
= link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
- - if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- - else
- .table-holder
- %table.table.builds.environments
- %thead
- %tr
- %th ID
- %th Commit
- %th Build
- %th
- %th
+ .deployments-container
+ - if @deployments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ %table.table.builds.environments
+ %thead
+ %tr
+ %th ID
+ %th Commit
+ %th Build
+ %th
+ %th.hidden-xs
- = render @deployments
+ = render @deployments
- = paginate @deployments, theme: 'gitlab'
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index cbdea209847..b94d6f8633c 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -55,12 +55,12 @@
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
+ = markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
= preserve do
- = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
+ = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ebf18f6ac85..ed23d06ee5e 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,13 +1,13 @@
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
+ = markdown_field(@merge_request, :title)
%div
- if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= 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
= @merge_request.description
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 73772cc0e32..e62f810a521 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -30,13 +30,13 @@
.detail-page-description.milestone-detail
%h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
+ = markdown_field(@milestone, :title)
%div
- if @milestone.description.present?
.description
.wiki
= preserve do
- = markdown @milestone.description
+ = markdown_field(@milestone, :description)
- if @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 788be4a0047..73fe6a715fa 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -61,7 +61,7 @@
.note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text.md
= 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)
- if note_editable
= render 'projects/notes/edit_form', note: note
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 5800ef7de48..d288efc546f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -33,7 +33,7 @@
- if @commit
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line
+ = markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
+ = preserve(markdown(@commit.description, pipeline: :single_line))
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 43a6fdfd103..d9c39fb87b7 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id
= 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
%span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 752b9e060d5..5afa193357e 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,8 +1,8 @@
%h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description
- - if shared_runners_text.present?
- = markdown(shared_runners_text, pipeline: 'plain_markdown')
+ - if current_application_settings.shared_runners_text.present?
+ = markdown_field(current_application_settings, :shared_runners_text)
- else
GitLab Shared Runners execute code of different projects on the same Runner
unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index a156d98bab8..05fccb4f976 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,4 +30,4 @@
.description.prepend-top-default
.wiki
= preserve do
- = markdown release.description
+ = markdown_field(release, :description)
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 4dd7439b2d0..155af755759 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -33,6 +33,6 @@
.description
.wiki
= preserve do
- = markdown @release.description
+ = markdown_field(@release, :description)
- else
This tag has no release notes.
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 8f68d6d1b87..e010f21de5a 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -7,7 +7,7 @@
- if issue.description.present?
.description.term
= 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
#{issue.project.name_with_namespace}
- if issue.closed?
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 6331c2bd6b0..07b17bc69c0 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
- if merge_request.description.present?
.description.term
= 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
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index b31595d8d1c..9664f65a36e 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
- if milestone.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(milestone.description))
+ = search_md_sanitize(milestone, :description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index e0400083870..f3701b89bb4 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -23,4 +23,4 @@
.note-search-result
.term
= preserve do
- = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
+ = search_md_sanitize(note, :note)
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 77676454b57..6f593e8dff9 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -12,4 +12,4 @@
= link_to_label(label, tooltip: false)
- if label.description
%span.label-description
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 1ad95351005..dc4ee3074d2 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -35,4 +35,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index b15e8ea73fe..33f93dccd3c 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -8,7 +8,7 @@
= link_to milestones_label_path(options) do
- render_colored_label(label, tooltip: false)
%span.prepend-description-left
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-info-right
%span.append-right-20
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 7ff947a51db..548215243db 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -26,7 +26,7 @@
.detail-page-description.milestone-detail
%h2.title
- = markdown escape_once(milestone.title), pipeline: :single_line
+ = markdown_field(milestone, :title)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
@@ -55,4 +55,3 @@
Open
%td
= ms.expires_at
-
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 66c309644a7..e8668048703 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -50,4 +50,4 @@
class: "commit-row-message"
- elsif project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 773ce8ac240..dcdba01aee9 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,9 +1,12 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
- = @snippet.data
+ = @snippet.content
.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
= render 'shared/file_highlight', blob: @snippet
- else
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 7ae4211ddfd..d7506e07ff6 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -21,4 +21,4 @@
= render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+ = markdown_field(@snippet, :title)
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
new file mode 100644
index 00000000000..c541daba50e
--- /dev/null
+++ b/app/workers/clear_database_cache_worker.rb
@@ -0,0 +1,23 @@
+# 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
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
new file mode 100644
index 00000000000..35e8b3808e2
--- /dev/null
+++ b/config/initializers/ar5_batching.rb
@@ -0,0 +1,41 @@
+# 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
diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb
index 7454c33c9dd..529dcdd4644 100644
--- a/config/initializers/gitlab_shell_secret_token.rb
+++ b/config/initializers/gitlab_shell_secret_token.rb
@@ -1 +1 @@
-Gitlab::Shell.new.generate_and_link_secret_token
+Gitlab::Shell.ensure_secret_token!
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
new file mode 100644
index 00000000000..8753e55e058
--- /dev/null
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -0,0 +1,38 @@
+# 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
diff --git a/db/schema.rb b/db/schema.rb
index ad62c249b3f..56da70b3c02 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -23,6 +23,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "message_html"
end
create_table "appearances", force: :cascade do |t|
@@ -30,8 +31,9 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "description"
t.string "header_logo"
t.string "logo"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "description_html"
end
create_table "application_settings", force: :cascade do |t|
@@ -92,6 +94,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.text "domain_blacklist"
t.boolean "koding_enabled"
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
create_table "audit_events", force: :cascade do |t|
@@ -128,13 +134,14 @@ ActiveRecord::Schema.define(version: 20160926145521) do
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
- t.text "message", null: false
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.datetime "created_at"
t.datetime "updated_at"
t.string "color"
t.string "font"
+ t.text "message_html"
end
create_table "ci_application_settings", force: :cascade do |t|
@@ -457,18 +464,20 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
+ t.integer "position", default: 0
t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.boolean "confidential", default: false
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -514,9 +523,10 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
+ t.boolean "template", default: false
t.string "description"
t.integer "priority"
+ t.text "description_html"
end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
@@ -632,6 +642,8 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.datetime "deleted_at"
t.string "in_progress_merge_commit_sha"
t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
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
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|
- t.string "title", null: false
- t.integer "project_id", null: false
+ t.string "title", null: false
+ t.integer "project_id", null: false
t.text "description"
t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
t.string "state"
t.integer "iid"
+ t.text "title_html"
+ t.text "description_html"
end
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
t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -721,6 +736,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "resolved_by_id"
t.string "discussion_id"
t.string "original_discussion_id"
+ t.text "note_html"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -872,6 +888,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -922,6 +939,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "description_html"
end
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
t.string "file_name"
t.string "type"
t.integer "visibility_level", default: 0, null: false
+ t.text "title_html"
+ t.text "content_html"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 869907b0dd7..27436a052da 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -20,7 +20,7 @@ Constants for project visibility levels are next:
## List projects
-Get a list of projects accessible by the authenticated user.
+Get a list of projects for which the authenticated user is a member.
```
GET /projects
@@ -28,11 +28,14 @@ GET /projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
```json
[
@@ -153,6 +156,138 @@ Parameters:
]
```
+Get a list of projects which the authenticated user can see.
+
+```
+GET /projects/visible
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
+ "web_url": "http://example.com/diaspora/diaspora-client",
+ "tag_list": [
+ "example",
+ "disapora client"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Diaspora Client",
+ "name_with_namespace": "Diaspora / Diaspora Client",
+ "path": "diaspora-client",
+ "path_with_namespace": "diaspora/diaspora-client",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
+ "http_url_to_repo": "http://example.com/brightbox/puppet.git",
+ "web_url": "http://example.com/brightbox/puppet",
+ "tag_list": [
+ "example",
+ "puppet"
+ ],
+ "owner": {
+ "id": 4,
+ "name": "Brightbox",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Puppet",
+ "name_with_namespace": "Brightbox / Puppet",
+ "path": "puppet",
+ "path_with_namespace": "brightbox/puppet",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 4,
+ "name": "Brightbox",
+ "owner_id": 1,
+ "path": "brightbox",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": false,
+ "avatar_url": null,
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+]
+```
+
### List owned projects
Get a list of projects which are owned by the authenticated user.
@@ -163,11 +298,13 @@ GET /projects/owned
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List starred projects
@@ -179,11 +316,13 @@ GET /projects/starred
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List ALL projects
@@ -195,11 +334,13 @@ GET /projects/all
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### Get single project
@@ -212,7 +353,9 @@ GET /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
{
@@ -301,7 +444,9 @@ GET /projects/:id/events
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
[
@@ -439,24 +584,26 @@ POST /projects
Parameters:
-- `name` (required) - new project name
-- `path` (optional) - custom repository name for new project. By default generated based on name
-- `namespace_id` (optional) - namespace for the new project (defaults to user)
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Create project for user
@@ -468,23 +615,27 @@ POST /projects/user/:user_id
Parameters:
-- `user_id` (required) - user_id of owner
-- `name` (required) - new project name
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The user ID of the project owner |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Edit project
@@ -496,24 +647,26 @@ PUT /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `name` (optional) - project name
-- `path` (optional) - repository name for project
-- `description` (optional) - short project description
-- `default_branch` (optional)
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `name` | string | yes | The name of the project |
+| `path` | string | no | Custom repository name for the project. By default generated based on name |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -528,8 +681,10 @@ POST /projects/fork/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `namespace` (optional) - The ID or path of the namespace that the project will be forked to
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
### Star a project
@@ -540,9 +695,11 @@ Stars a given project. Returns status code `201` and the project on success and
POST /projects/:id/star
```
+Parameters:
+
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -610,7 +767,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -682,7 +839,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
@@ -770,7 +927,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
@@ -853,7 +1010,9 @@ DELETE /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Uploads
@@ -867,8 +1026,10 @@ POST /projects/:id/uploads
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `file` (required) - The file to be uploaded
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `file` | string | yes | The file to be uploaded |
```json
{
@@ -896,10 +1057,12 @@ POST /projects/:id/share
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `group_id` (required) - The ID of a group
-- `group_access` (required) - Level of permissions for sharing
-- `expires_at` - Share expiration date in ISO 8601 format: 2016-09-26
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `group_id` | integer | yes | The ID of the group to share with |
+| `group_access` | integer | yes | The permissions level to grant the group |
+| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
## Hooks
@@ -916,7 +1079,9 @@ GET /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
### Get project hook
@@ -928,8 +1093,10 @@ GET /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of a project hook |
```json
{
@@ -959,17 +1126,19 @@ POST /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
+| `tag_push_events` | boolean | no | Trigger hook on tag push events |
+| `note_events` | boolean | no | Trigger hook on note events |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Edit project hook
@@ -981,18 +1150,20 @@ PUT /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on issues events |
+| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
+| `tag_push_events` | boolean | no | Trigger hook on tag push events |
+| `note_events` | boolean | no | Trigger hook on note events |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Delete project hook
@@ -1005,8 +1176,10 @@ DELETE /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of hook to delete
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
Note the JSON response differs if the hook is available or not. If the project hook
is available before it is returned in the JSON response or an empty response is returned.
@@ -1025,7 +1198,9 @@ GET /projects/:id/repository/branches
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```json
[
@@ -1080,10 +1255,12 @@ GET /projects/:id/repository/branches/:branch
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
-- `developers_can_push` - Flag if developers can push to the branch.
-- `developers_can_merge` - Flag if developers can merge to the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
+| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
+| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
### Protect single branch
@@ -1095,8 +1272,10 @@ PUT /projects/:id/repository/branches/:branch/protect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
### Unprotect single branch
@@ -1108,8 +1287,10 @@ PUT /projects/:id/repository/branches/:branch/unprotect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
## Admin fork relation
@@ -1123,8 +1304,10 @@ POST /projects/:id/fork/:forked_from_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `forked_from_id:` (required) - The ID of the project that was forked from
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `forked_from_id` | ID | yes | The ID of the project that was forked from |
### Delete an existing forked from relationship
@@ -1134,7 +1317,9 @@ DELETE /projects/:id/fork
Parameter:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Search for projects by name
@@ -1146,8 +1331,10 @@ GET /projects/search/:query
Parameters:
-- `query` (required) - A string contained in the project name
-- `per_page` (optional) - number of projects to return per page
-- `page` (optional) - the page to retrieve
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `query` (required) - A string contained in the project name
+| `per_page` (optional) - number of projects to return per page
+| `page` (optional) - the page to retrieve
+| `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
diff --git a/doc/university/README.md b/doc/university/README.md
index 6ca1c20c9b2..8b3538d5616 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -1,139 +1,215 @@
+# GitLab University
-## What is GitLab University
+GitLab University is the best place to learn about **Version Control with Git and GitLab**.
-_GitLab University_ has as a goal to teach the fundamentals of **Version Control with Git and GitLab** through courses that cover topics which can be mastered in around 2 hours.
+It doesn't replace, but accompanies our great [Documentation](http://docs.gitlab.com)
+and [Blog Articles](https://about.gitlab.com/blog/).
-_University materials don't replace our [Documentation](http://docs.gitlab.com) or [Blog Articles](https://about.gitlab.com/blog/)._
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
----
+## Gitlab University Curriculum
+
+The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections.
-### On this page
-
-+ [GITx] Git
-+ [OPSx] DevOps
-+ [GLBx] GitLab Basics
-+ [INTx] GitLab Integrations
-+ [GLFx] GitLab Workflows
-+ [GLEx] GitLab Enterprise Edition extra features
-+ [GCIx] GitLab CI
-+ [ECO] Ecosystem
-+ [COM] Competition comparison
-+ [SPTx] Support Bootcamp
-+ [SLSx] Sales Bootcamp
-+ [TRAx] Trainings
+1. [GitLab Beginner](#beginner)
+1. [GitLab Intermediate](#intermediate)
+1. [GitLab Advanced](#advanced)
+1. [External Articles](#external)
+1. [Resources for GitLab Team Members](#team)
---
-+ [GIT1] [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
-+ [GIT2] [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
-+ [GIT3] [Intro to Git](https://www.codeschool.com/account/courses/try-git)
+### 1. <a name="beginner"></a> GitLab Beginner
----
+#### 1.1. Version Control and Git
-+ [OPS1] [What is Omnibus](https://www.youtube.com/watch?v=XTmpKudd-Oo)
-+ [OPS2] [Installing GitLab](https://www.youtube.com/watch?v=Q69YaOjqNhg)
-+ [OPS3] [Configuring an external PostgreSQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
-+ [OPS5] [Importing from Other Tools or SVN](http://doc.gitlab.com/ee/workflow/importing/)
-+ [OPS6] [High Availability Documentation](https://about.gitlab.com/high-availability/)
-+ [OPS7] [Managing LDAP, Active Directory](https://www.youtube.com/watch?v=HPMjM-14qa8)
-+ [OPS8] [Scalability and High Availability](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
-+ [OPS9] [High Availability on AWS](high-availability/aws/README.md)
+1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
+1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
+1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
----
+#### 1.2. GitLab Basics
-+ [GLB1] [Terminology](glossary/README.md)
-+ [GLB2] [GitLab Basics](http://doc.gitlab.com/ce/gitlab-basics/README.html)
-+ [GLB3] [Demo of GitLab.com](https://www.youtube.com/watch?v=WaiL5DGEMR4)
-+ [GLB4] [Create and Add your SSH key to GitLab](https://www.youtube.com/watch?v=54mxyLo3Mqk)
-+ [GLB5] [Repositories, Projects and Groups](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
-+ [GLB6] [Creating a Project in GitLab](https://www.youtube.com/watch?v=7p0hrpNaJ14)
-+ [GLB7] [Issues and Merge Requests](https://www.youtube.com/watch?v=raXvuwet78M)
-+ [GLB8] [Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/git_lfs_and_annex.md)
+1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
+1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
+1. [GitLab Basics - Article](http://doc.gitlab.com/ce/gitlab-basics/README.html)
+1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
+1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
----
+#### 1.3. Your GitLab Account
-+ [INT1] [JIRA and Jenkins integrations in GitLab](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
-+ [INT2] [Integrating JIRA with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
-+ [INT3] [Integrating Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
-+ [INT4] [Integrating Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
-+ [INT5] [Documentation on Integrating Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
----
+#### 1.4. GitLab Projects
-+ [GLF1] [GitLab Flow](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [Repositories, Projects and Groups - Video](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Creating a Project in GitLab - Video](https://www.youtube.com/watch?v=7p0hrpNaJ14)
+1. [How to Create Files and Directories](https://about.gitlab.com/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
+1. [GitLab Todos](https://about.gitlab.com/2016/03/02/gitlab-todos-feature-highlight/)
+1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/2016/01/08/feature-highlight-wip/)
----
+#### 1.5. Migrating from other Source Control
-+ [GLE1] [Configuring an external MySQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
-+ [GLE2] [Managing Permissions within EE](https://www.youtube.com/watch?v=DjUoIrkiNuM)
-+ [GLE3] [Upcoming in EE and Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/upcoming_in_ee.md)
+1. [Migrating from BitBucket/Stash](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
+1. [Migrating from GitHub](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
+1. [Migrating from SVN](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+1. [Migrating from Fogbugz](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
----
+#### 1.6. GitLab Inc.
-+ [GCI1] [GitLab CI product page](https://about.gitlab.com/gitlab-ci/)
-+ [GCI2] [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+1. [About GitLab](https://about.gitlab.com/about/)
+1. [GitLab Direction](https://about.gitlab.com/direction/)
+1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
+1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
+1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
----
+#### 1.7 Community and Support
-+ [COM1] [GitLab compared to other tools](https://about.gitlab.com/comparison/)
-+ [COM2] [Compare GitLab versions](https://about.gitlab.com/features/#compare)
-+ [COM3] [Innersourcing article](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [Getting Help](/getting-help/)
+ - Proposing Features and Reporting and Tracking bugs for GitLab
+ - The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
+ - Getting Technical Support
+ - Being part of our Great Community and Contributing to GitLab
+1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
+1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
+1. [GitLab Training Workshops](/training)
----
+#### 1.8 GitLab Training Material
-+ [ECO1] [Ecosystem Overview](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6)
-+ [ECO2] [Positioning FAQ](https://about.gitlab.com/handbook/positioning-faq)
-+ [ECO3] [GitLab Ecosystem slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
-+ [ECO4] [Customer Use-Cases](https://about.gitlab.com/handbook/use-cases/)
+1. [Git and GitLab Terminology](/glossary/)
+1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
+1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
---
-+ [SPT1] [Support Path](support/README.md)
-+ [SPT2] [End User Training Material](https://gitlab.com/gitlab-org/University/blob/master/training/user_training.md)
-+ [SPT3] [Materials for Training Sessions](https://gitlab.com/gitlab-org/University/tree/master/training/topics)
+### 2. <a name="intermediate"></a> GitLab Intermediate
+
+#### 2.1 GitLab Pages
+
+1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+1. [GitLab Pages Documentation](http://doc.gitlab.com/ee/pages/README.html)
+
+#### 2.2. GitLab Issues
+
+1. [Markdown in GitLab](http://doc.gitlab.com/ce/markdown/markdown.html)
+1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
+1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
+1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
+1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
+1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+
+#### 2.3. Continuous Integration
+
+1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
+1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
+1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [How we scale GitLab with built in Docker](https://about.gitlab.com/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
+1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+1. [Deployments and Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
+1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
+1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
+1. See **[Integrations](#integrations)** for integrations with other CI services.
+
+#### 2.4. Workflow
+
+1. [GitLab Flow - Video](https://youtu.be/enMumwvLAug?list=PLFGfElNsQthZnwMUFi6rqkyUZkI00OxIV)
+1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
+1. [GitLab Flow Documentation](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
+
+#### 2.5. GitLab Comparisons
+
+1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
+1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
+1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
---
-+ [SLS1] [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
-+ [SLS2] [GitLab Direction](https://about.gitlab.com/direction/)
+### 3. <a name="advanced"></a> GitLab Advanced
+
+#### 3.1. Dev Ops
+
+1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
+1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
+1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+
+#### 3.2. Installing GitLab with Omnibus
+
+1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
+1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
+1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
+1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
+1. [Using a MySQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
+1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
+1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
+
+#### 3.3. Permissions
+
+1. [How to Manage Permissions in GitLab EE - Video](https://www.youtube.com/watch?v=DjUoIrkiNuM)
+
+#### 3.4. Large Files
+
+1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+
+#### 3.5. LDAP and Active Directory
+
+1. [How to Manage LDAP, Active Directory in GitLab - Video](https://www.youtube.com/watch?v=HPMjM-14qa8)
+
+#### 3.6 Custom Languages
+
+1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+
+#### 3.7. Scalability and High Availability
+
+1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
+1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
+
+#### 3.8 Cycle Analytics
+
+1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+
+#### 3.9. <a name="integrations"></a> Integrations
+
+1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
+1. [How to Integrate Jira with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
+1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
+1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
---
-+ [TRA1] [End User Training](training/end-user/README.md)
+## 4. <a name="external"></a> External Articles
+
+1. [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
+1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
---
-### External Resources
-
-+ [DOC] GitLab Documentation
- + [Set up and use GitLab Pages](http://doc.gitlab.com/ee/pages/README.html)
- + [Markdown Reference](http://doc.gitlab.com/ce/markdown/markdown.html)
-
-+ [GLW] GitLab Workshop (@ Platzi)
- + [GitLab Workshop Part 1: Basics of Git and GitLab](https://courses.platzi.com/classes/git-gitlab/)
- + [Create a GitLab Account](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
-
-+ [GLY] GitLab YouTube Videos
- + [Making GitLab Great for Everyone, our response to the Dear GitHub letter](https://www.youtube.com/watch?v=GGC40y4vMx0)
- + [Compared to Atlassian (Recorded on 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
-
-+ [GLI] GitLab Team-Only Access
- + [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
- + [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
-
-+ [KNT] Slides & Keynotes by GitLabbers & other individuals
- + [Why Git and GitLab slide deck](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/)
- + [Git Workshop](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/)
-
-+ Others (not created by GitLab)
- + [Dev Ops terminology](https://xebialabs.com/glossary/)
- + [Continuous Delivery vs Continuous Deployment](https://www.youtube.com/watch?v=igwFj8PPSnw)
- + [Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
- + [State of Dev Ops 2015 Report by Puppet Labs](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf) Insightful Chapters to understand the Impact of Continuous Delivery on Performance (Chapter 4), the Application Architecture (Chapter 5) and How IT Managers can help their teams win (Chapter 6).
- + [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
- + [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
- + [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
- + [Customer review of GitLab with talking points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
- + [3rd party tool comparison](http://technologyconversations.com/2015/10/16/github-vs-gitlabs-vs-bitbucket-server-formerly-stash/)
- + [Amazon's transition to Continuous Delivery](https://www.youtube.com/watch?v=esEFaY0FDKc)
- + [Article on Continuous Integration from ThoughtWorks](https://www.thoughtworks.com/continuous-integration)
+## 5. <a name="team"></a> Resources for GitLab Team Members
+
+*Some content can only be accessed by GitLab team members*
+
+1. [Support Path](/support/)
+1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
+1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index d57c0d0674d..bfb83cf79b1 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -99,6 +99,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 46dfa2232b4..7f36ce00e96 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -116,6 +116,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index b24d338e3e0..119c5f475e4 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -158,6 +158,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index ee9fb1a2a68..07743d050f7 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -166,6 +166,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index e3f61247be5..00d63c1b3c6 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -166,6 +166,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 9f5c6c4dc84..dd3fdafd8d1 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -158,6 +158,10 @@ it where the 'public' directory of GitLab is.
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Use Redis v2.8.0+
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 9f6517d9487..e62d894609a 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -98,6 +98,10 @@ We updated the init script for GitLab in order to set a specific PATH for gitlab
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0cb137a03cc..678cc69d773 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -119,6 +119,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 6267f14eba4..a76346516b9 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -138,6 +138,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index cb66ef920bb..05ef4e61759 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 32906650f6f..8ce434e5f78 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index f078a2bece5..aa077316bbe 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index a057a423e61..bb2c79fbb84 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8b8c4eb4d46..67473f300c9 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -25,7 +25,7 @@ module API
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
- warden.try(:authenticate) if request.get? || request.head?
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
@@ -433,7 +433,7 @@ module API
end
def secret_token
- File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ Gitlab::Shell.secret_token
end
def send_git_blob(repository, blob)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 680055c95eb..c24e8e8bd9b 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -22,14 +22,25 @@ module API
# Example Request:
# GET /projects
get do
- @projects = current_user.authorized_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- if params[:simple]
- present @projects, with: Entities::BasicProjectDetails, user: current_user
- else
- present @projects, with: Entities::ProjectWithAccess, user: current_user
- end
+ projects = current_user.authorized_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
+ end
+
+ # Get a list of visible projects for authenticated user
+ #
+ # Example Request:
+ # GET /projects/visible
+ get '/visible' do
+ projects = ProjectsFinder.new.execute(current_user)
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
end
# Get an owned projects list for authenticated user
@@ -37,10 +48,10 @@ module API
# Example Request:
# GET /projects/owned
get '/owned' do
- @projects = current_user.owned_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = current_user.owned_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Gets starred project for the authenticated user
@@ -48,10 +59,10 @@ module API
# Example Request:
# GET /projects/starred
get '/starred' do
- @projects = current_user.viewable_starred_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::Project, user: current_user
+ projects = current_user.viewable_starred_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::Project, user: current_user
end
# Get all projects for admin user
@@ -60,10 +71,10 @@ module API
# GET /projects/all
get '/all' do
authenticated_as_admin!
- @projects = Project.all
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = Project.all
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get a single project
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 9ebe379f454..35ca234c1ba 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,6 +3,10 @@ module Banzai
Renderer.render(text, context)
end
+ def self.render_field(object, field)
+ Renderer.render_field(object, field)
+ end
+
def self.cache_collection_render(texts_and_contexts)
Renderer.cache_collection_render(texts_and_contexts)
end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
new file mode 100644
index 00000000000..4ef8b3b6dcf
--- /dev/null
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -0,0 +1,12 @@
+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
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
index bab6a9934d1..2b7c10f1a0e 100644
--- a/lib/banzai/note_renderer.rb
+++ b/lib/banzai/note_renderer.rb
@@ -3,7 +3,7 @@ module Banzai
# Renders a collection of Note instances.
#
# 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.
# path - The request path.
# wiki - The project's wiki.
@@ -13,8 +13,7 @@ module Banzai
user,
requested_path: path,
project_wiki: wiki,
- ref: git_ref,
- pipeline: :note)
+ ref: git_ref)
renderer.render(notes, :note)
end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 9aef807c152..9f8eb0931b8 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -1,28 +1,32 @@
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
- # name of the attribute containing the Markdown. For example, when the
- # attribute `note` is rendered the HTML is stored in `note_html`.
+ # The unredacted HTML is generated according to the usual +render_field+
+ # policy, so specify the pipeline and any other context options on the model.
+ #
+ # 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
attr_reader :project, :user
- # Make sure to set the appropriate pipeline in the `raw_context` attribute
- # (e.g. `:note` for Note instances).
- #
- # project - A Project to use for rendering and redacting Markdown.
+ # project - A Project to use for redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any.
- # context - A Hash containing extra attributes to use in the rendering
- # pipeline.
- def initialize(project, user = nil, raw_context = {})
+ # context - A Hash containing extra attributes to use during redaction
+ def initialize(project, user = nil, redaction_context = {})
@project = project
@user = user
- @raw_context = raw_context
+ @redaction_context = redaction_context
end
# 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.
#
# Returns the same input objects.
@@ -32,7 +36,7 @@ module Banzai
objects.each_with_index do |object, 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]
end
end
@@ -53,12 +57,8 @@ module Banzai
# Returns a Banzai context for the given object and attribute.
def context_for(object, attribute)
- context = base_context.merge(cache_key: [object, attribute])
-
- if object.respond_to?(:author)
- context[:author] = object.author
- end
-
+ context = base_context.dup
+ context = context.merge(object.banzai_render_context(attribute))
context
end
@@ -66,21 +66,16 @@ module Banzai
#
# Returns an Array of `Nokogiri::HTML::Document`.
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)
- string = object.__send__(attribute)
-
- { 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])
+ Banzai::Pipeline[:relative_link].to_document(string, context)
end
end
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
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index ba2555df98d..30bc035d085 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -3,6 +3,7 @@ module Banzai
class SingleLinePipeline < GfmPipeline
def self.filters
@filters ||= FilterArray[
+ Filter::HTMLEntityFilter,
Filter::SanitizationFilter,
Filter::EmojiFilter,
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index a4ae27eefd8..6924a293da8 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -31,6 +31,34 @@ module Banzai
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
# Array of HTML-safe String of HTML.
#
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 79eac66b364..d0060fbaca1 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -17,6 +17,18 @@ module Gitlab
end
class << self
+ def secret_token
+ @secret_token ||= begin
+ File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ end
+ end
+
+ def ensure_secret_token!
+ return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret'))
+
+ generate_and_link_secret_token
+ end
+
def version_required
@version_required ||= File.read(Rails.root.
join('GITLAB_SHELL_VERSION')).strip
@@ -25,6 +37,25 @@ module Gitlab
def strip_key(key)
key.split(/ /)[0, 2].join(' ')
end
+
+ private
+
+ # Create (if necessary) and link the secret token file
+ def generate_and_link_secret_token
+ secret_file = Gitlab.config.gitlab_shell.secret_file
+ shell_path = Gitlab.config.gitlab_shell.path
+
+ unless File.size?(secret_file)
+ # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
+ token = SecureRandom.hex(16)
+ File.write(secret_file, token)
+ end
+
+ link_path = File.join(shell_path, '.gitlab_shell_secret')
+ if File.exist?(shell_path) && !File.exist?(link_path)
+ FileUtils.symlink(secret_file, link_path)
+ end
+ end
end
# Init new repository
@@ -201,21 +232,6 @@ module Gitlab
File.exist?(full_path(storage, dir_name))
end
- # Create (if necessary) and link the secret token file
- def generate_and_link_secret_token
- secret_file = Gitlab.config.gitlab_shell.secret_file
- unless File.size?(secret_file)
- # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
- token = SecureRandom.hex(16)
- File.write(secret_file, token)
- end
-
- link_path = File.join(gitlab_shell_path, '.gitlab_shell_secret')
- if File.exist?(gitlab_shell_path) && !File.exist?(link_path)
- FileUtils.symlink(secret_file, link_path)
- end
- end
-
protected
def gitlab_shell_path
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 2214f855200..a95a3455a4a 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,22 +1,33 @@
namespace :cache do
- 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
+ namespace :clear do
+ 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"
- task :clear => :environment do
- Gitlab::Redis.with do |redis|
- cursor = REDIS_SCAN_START_STOP
- loop do
- cursor, keys = redis.scan(
- cursor,
- match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
- count: CLEAR_BATCH_SIZE
- )
-
- redis.del(*keys) if keys.any?
-
- break if cursor == REDIS_SCAN_START_STOP
+ desc "GitLab | Clear redis cache"
+ task redis: :environment do
+ Gitlab::Redis.with do |redis|
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
+ count: REDIS_CLEAR_BATCH_SIZE
+ )
+
+ redis.del(*keys) if keys.any?
+
+ break if cursor == REDIS_SCAN_START_STOP
+ end
end
end
+
+ desc "GitLab | Clear database cache (in the background)"
+ task db: :environment do
+ ClearDatabaseCacheWorker.perform_async
+ end
+
+ task all: [:db, :redis]
end
+
+ task clear: 'cache:clear:all'
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index bb7eb852f1b..210899882b4 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -78,7 +78,7 @@ namespace :gitlab do
f.puts "PATH=#{ENV['PATH']}"
end
- Gitlab::Shell.new.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
desc "GitLab | Setup gitlab-shell"
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 873d3fcb5af..331172445e4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -9,6 +9,9 @@ FactoryGirl.define do
namespace
creator
+ # Behaves differently to nil due to cache_has_external_issue_tracker
+ has_external_issue_tracker false
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
@@ -92,6 +95,8 @@ FactoryGirl.define do
end
factory :redmine_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_redmine_service(
active: true,
@@ -105,6 +110,8 @@ FactoryGirl.define do
end
factory :jira_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_jira_service(
active: true,
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 4309a726917..68ea4eeae31 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -44,6 +44,10 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
+
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
+ end
context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
@@ -61,6 +65,20 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
end
end
@@ -122,6 +140,16 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
end
end
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 157cc4665a2..c6e3c5c2368 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do
end
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)
@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do
end
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')
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 67bac782591..abe08d95ece 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -63,28 +63,38 @@ describe IssuesHelper do
end
describe '#award_user_list' do
- let!(:awards) { build_list(:award_emoji, 15) }
+ it "returns a comma-separated list of the first X users" do
+ user = build_stubbed(:user, name: 'Joe')
+ awards = Array.new(3, build_stubbed(:award_emoji, user: user))
- it "returns a comma seperated list of 1-9 users" do
- expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Joe, Joe, and Joe')
end
it "displays the current user's name as 'You'" do
- expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
- end
+ user = build_stubbed(:user, name: 'Joe')
+ award = build_stubbed(:award_emoji, user: user)
- it "truncates lists of larger than 9 users" do
- expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
+ expect(award_user_list([award], user)).to eq('You')
+ expect(award_user_list([award], nil)).to eq 'Joe'
end
- it "displays the current user in front of 0-9 other users" do
- expect(award_user_list(awards, awards[0].user)).
- to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "truncates lists" do
+ user = build_stubbed(:user, name: 'Jane')
+ awards = Array.new(5, build_stubbed(:award_emoji, user: user))
+
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Jane, Jane, Jane, and 2 more.')
end
- it "displays the current user in front regardless of position in the list" do
- expect(award_user_list(awards, awards[12].user)).
- to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "displays the current user in front of other users" do
+ current_user = build_stubbed(:user)
+ my_award = build_stubbed(:award_emoji, user: current_user)
+ award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
+ awards = Array.new(5, award).push(my_award)
+
+ expect(award_user_list(awards, current_user, limit: 2)).
+ to eq("You, Jane, and 4 more.")
end
end
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
new file mode 100644
index 00000000000..6dc4a970071
--- /dev/null
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -0,0 +1,14 @@
+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
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
index 98f76f36fd5..49556074278 100644
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do
with(project, user,
requested_path: 'foo',
project_wiki: wiki,
- ref: 'bar',
- pipeline: :note).
+ ref: 'bar').
and_call_original
expect_any_instance_of(Banzai::ObjectRenderer).
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index bcdb95250ca..f5ff236105e 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do
let(:project) { create(:empty_project) }
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
it 'renders and redacts an Array of objects' do
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).
and_call_original
@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)).
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)
renderer.render([object], :note)
@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do
describe '#render_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)
@@ -57,49 +65,29 @@ describe Banzai::ObjectRenderer do
end
describe '#context_for' do
- let(:object) { double(:object, note: 'hello') }
+ let(:object) { fake_object(note: 'hello') }
let(:renderer) { described_class.new(project, user) }
it 'returns a Hash' do
expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
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)
-
- expect(context[:cache_key]).to eq([object, :note])
- 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
+ expect(context).to have_key(:foo)
+ expect(context[:foo]).to eq(:bar)
end
end
describe '#render_attributes' do
it 'renders the attribute of a list of objects' do
- objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
- renderer = described_class.new(project, user, pipeline: :note)
+ objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)]
+ renderer = described_class.new(project, user)
- expect(Banzai).to receive(:cache_collection_render).
- with([
- { text: 'hello', context: renderer.context_for(objects[0], :note) },
- { text: 'bye', context: renderer.context_for(objects[1], :note) }
- ]).
- and_call_original
+ objects.each do |object|
+ expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
+ end
docs = renderer.render_attributes(objects, :note)
@@ -114,17 +102,13 @@ describe Banzai::ObjectRenderer do
objects = []
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([])
end
end
describe '#base_context' do
let(:context) do
- described_class.new(project, user, pipeline: :note).base_context
+ described_class.new(project, user, foo: :bar).base_context
end
it 'returns a Hash' do
@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do
end
it 'includes the custom attributes' do
- expect(context[:pipeline]).to eq(:note)
+ expect(context[:foo]).to eq(:bar)
end
it 'includes the current user' do
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
new file mode 100644
index 00000000000..aaa6b12e67e
--- /dev/null
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -0,0 +1,74 @@
+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
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 07407f212aa..f826d0d1b04 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -22,15 +22,15 @@ describe Gitlab::Shell, lib: true do
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
- describe 'generate_and_link_secret_token' do
+ describe 'memoized secret_token' do
let(:secret_file) { 'tmp/tests/.secret_shell_test' }
let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
before do
- allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
FileUtils.mkdir('tmp/tests/shell-secret-test')
- gitlab_shell.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
after do
@@ -39,7 +39,10 @@ describe Gitlab::Shell, lib: true do
end
it 'creates and links the secret token file' do
+ secret_token = Gitlab::Shell.secret_token
+
expect(File.exist?(secret_file)).to be(true)
+ expect(File.read(secret_file).chomp).to eq(secret_token)
expect(File.symlink?(link_file)).to be(true)
expect(File.readlink(link_file)).to eq(secret_file)
end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 2e19d590d83..ea65a5dfed1 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -26,10 +26,11 @@ describe 'Import/Export attribute configuration', lib: true do
it 'has no new columns' do
relation_names.each do |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"
- 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]
new_attributes = current_attributes - safe_attributes
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 305f8bc88cc..c4486a32082 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:reporter).class_name('User') }
it { is_expected.to belong_to(:user) }
+
+ it "aliases reporter to author" do
+ expect(subject.author).to be(subject.reporter)
+ end
end
describe 'validations' do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
new file mode 100644
index 00000000000..15cd3a7ed70
--- /dev/null
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -0,0 +1,181 @@
+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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e52d4aaf884..8aadfcb439b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -308,7 +308,9 @@ describe Project, models: true do
end
describe 'last_activity methods' do
- let(:project) { create(:project, last_activity_at: 2.hours.ago) }
+ let(:timestamp) { 2.hours.ago }
+ # last_activity_at gets set to created_at upon creation
+ let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
it 'alias last_activity to last_event' do
@@ -322,6 +324,7 @@ describe Project, models: true do
it 'returns the creation date of the project\'s last event if present' do
new_event = create(:event, project: project, created_at: Time.now)
+ project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
@@ -518,7 +521,7 @@ describe Project, models: true do
end
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
services = double(:service, external_issue_trackers: [RedmineService.new])
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index ed1bc9271ae..43937a54b2c 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -238,7 +238,7 @@ describe Service, models: true do
it "updates the has_external_issue_tracker boolean" do
expect do
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
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index e6bc5296398..f62f6bacbaa 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -46,6 +46,13 @@ describe Snippet, models: true do
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
let(:snippet) { create(:snippet) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 4a0d727faea..5f19638b460 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -175,6 +175,36 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/visible' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ public_project
+ project
+ project2
+ project3
+ project4
+ end
+
+ it 'returns the projects viewable by the user' do
+ get api('/projects/visible', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+ end
+
+ it 'shows only public projects when the user only has access to those' do
+ get api('/projects/visible', user2)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id)
+ end
+ end
+
describe 'GET /projects/starred' do
let(:public_project) { create(:project, :public) }
@@ -232,7 +262,7 @@ describe API::API, api: true do
post api('/projects', user), project
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)
end
@@ -360,7 +390,7 @@ describe API::API, api: true do
post api("/projects/user/#{user.id}", admin), project
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)
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 22991c5bc86..8e3e12114f2 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -448,6 +448,8 @@ describe GitPushService, services: true do
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do
+ # project.create_jira_service doesn't seem to invalidate the cache here
+ project.has_external_issue_tracker = true
jira_service_settings
WebMock.stub_request(:post, jira_api_transition_url)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index e49a0d5e553..ee53e110aee 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -60,7 +60,10 @@ describe MergeRequests::MergeService, services: true do
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
jira_issue = ExternalIssue.new('JIRA-123', project)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index d1a47ea9b6f..304d4e62396 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -531,12 +531,12 @@ describe SystemNoteService, services: true do
include JiraServiceHelper
describe 'JIRA integration' do
- let(:project) { create(:project) }
+ let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: 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 }
context 'in JIRA issue tracker' do
@@ -545,10 +545,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:post, jira_api_comment_url)
end
- after do
- jira_tracker.destroy!
- end
-
describe "new reference" do
before do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
@@ -578,10 +574,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end
- after do
- jira_tracker.destroy!
- end
-
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(jira_status_message) }