Commit 58278d0c authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-02-05'

parents 5c34451b 20b7818d
......@@ -51,6 +51,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
## Contribute to GitLab
For a first-time step-by-step guide to the contribution process, see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
......
......@@ -72,6 +72,10 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
# Before updating this gem, check if
# https://github.com/gollum/gollum-lib/pull/292 has been merged.
# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer
# in config/initializers/gollum.rb
gem 'gollum-lib', '~> 4.2', require: false
# Before updating this gem, check if
......
......@@ -235,7 +235,7 @@ export default class FileTemplateMediator {
}
setFilename(name) {
this.$filenameInput.val(name);
this.$filenameInput.val(name).trigger('change');
}
getSelected() {
......
......@@ -461,7 +461,7 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
return regexp.exec(targetSubtext);
}
......
......@@ -16,3 +16,31 @@
background-color: $user-mention-bg-hover;
}
}
.gfm-color_chip {
display: inline-block;
margin: 0 0 2px 4px;
vertical-align: middle;
border-radius: 3px;
$chip-size: 0.9em;
$bg-size: $chip-size / 0.9;
$bg-pos: $bg-size / 2;
width: $chip-size;
height: $chip-size;
background: $white-light;
background-image: linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%),
linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%);
background-size: $bg-size $bg-size;
background-position: 0 0, $bg-pos $bg-pos;
> span {
display: inline-block;
width: 100%;
height: 100%;
margin-bottom: 2px;
border-radius: 3px;
border: 1px solid $black-transparent;
}
}
......@@ -6,6 +6,14 @@
}
}
.wiki-form {
.edit-wiki-page-slug-tip {
display: inline-block;
max-width: 100%;
margin-top: 5px;
}
}
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
......
......@@ -70,7 +70,7 @@ module UploadsActions
end
def build_uploader_from_params
uploader = uploader_class.new(model, params[:secret])
uploader = uploader_class.new(model, secret: params[:secret])
uploader.retrieve_from_store!(params[:filename])
uploader
end
......
......@@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
rescue WikiPage::PageChangedError
@conflict = true
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
@error = e
render 'edit'
end
......
class UserCalloutsController < ApplicationController
def create
if ensure_callout.persisted?
respond_to do |format|
format.json { head :ok }
end
else
respond_to do |format|
format.json { head :bad_request }
end
end
end
private
def ensure_callout
current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name])
end
def feature_name
params.require(:feature_name)
end
end
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
private
def user_dismissed?(feature_name)
current_user&.callouts&.find_by(feature_name: feature_name)
end
end
......@@ -21,4 +21,22 @@ module WikiHelper
add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
end
end
def wiki_page_errors(error)
return unless error
content_tag(:div, class: 'alert alert-danger') do
case error
when WikiPage::PageChangedError
page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
concat(
(s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
)
when WikiPage::PageRenameError
s_("WikiEdit|There is already a page with the same title in that path.")
else
error.message
end
end
end
end
......@@ -87,20 +87,10 @@ module Storage
remove_exports!
end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def remove_legacy_exports!
legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was)
def full_path_was
if parent
parent.full_path + '/' + path_was
else
path_was
end
FileUtils.rm_rf(legacy_export_path)
end
end
end
......@@ -312,12 +312,6 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
end
def group_member(user)
if group_members.loaded?
group_members.find { |gm| gm.user_id == user.id }
......
......@@ -36,9 +36,8 @@ class Key < ActiveRecord::Base
after_destroy :refresh_user_cache
def key=(value)
value&.delete!("\n\r")
value.strip! unless value.blank?
write_attribute(:key, value)
write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil)
@public_key = nil
end
......@@ -100,7 +99,7 @@ class Key < ActiveRecord::Base
def generate_fingerprint
self.fingerprint = nil
return unless self.key.present?
return unless public_key.valid?
self.fingerprint = public_key.fingerprint
end
......
......@@ -235,6 +235,24 @@ class Namespace < ActiveRecord::Base
feature_available?(:multiple_issue_boards)
end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
end
# Exports belonging to projects with legacy storage are placed in a common
# subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove
# them.
#
# Exports of projects using hashed storage are placed in a location defined
# only by the project ID, so each must be removed individually.
def remove_exports!
remove_legacy_exports!
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
private
def refresh_access_of_projects_invited_groups
......
......@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base
before_destroy :remove_private_deploy_keys
after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
after_validation :check_pending_delete
......@@ -1537,6 +1538,8 @@ class Project < ActiveRecord::Base
end
def export_path
return nil unless namespace.present? || hashed_storage?(:repository)
File.join(Gitlab::ImportExport.storage_path, disk_path)
end
......@@ -1545,8 +1548,9 @@ class Project < ActiveRecord::Base
end
def remove_exports
_, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
status.zero?
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
def full_path_slug
......
......@@ -131,6 +131,8 @@ class ProjectWiki
end
def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_elastic_index
......@@ -145,6 +147,8 @@ class ProjectWiki
end
def page_title_and_dir(title)
return unless title
title_array = title.split("/")
title = title_array.pop
[title, title_array.join("/")]
......
......@@ -180,15 +180,7 @@ class Repository
end
def find_branch(name, fresh_repo: true)
# Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
# cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
# a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
# may cause the branch to "disappear" erroneously or have the wrong SHA.
#
# See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
raw_repo = fresh_repo ? initialize_raw_repository : raw_repository
raw_repo.find_branch(name)
raw_repository.find_branch(name, fresh_repo)
end
def find_tag(name)
......@@ -728,11 +720,11 @@ class Repository
end
def branch_names_contains(sha)
refs_contains_sha('branch', sha)
raw_repository.branch_names_contains_sha(sha)
end
def tag_names_contains(sha)
refs_contains_sha('tag', sha)
raw_repository.tag_names_contains_sha(sha)
end
def local_branches
......
......@@ -33,7 +33,7 @@ class Upload < ActiveRecord::Base
end
def build_uploader
uploader_class.new(model).tap do |uploader|
uploader_class.new(model, mount_point, **uploader_context).tap do |uploader|
uploader.upload = self
uploader.retrieve_from_store!(identifier)
end
......@@ -43,6 +43,13 @@ class Upload < ActiveRecord::Base
File.exist?(absolute_path)
end
def uploader_context
{
identifier: identifier,
secret: secret
}.compact
end
private
def checksummable?
......@@ -67,11 +74,15 @@ class Upload < ActiveRecord::Base
!path.start_with?('/')
end
def uploader_class
Object.const_get(uploader)
end
def identifier
File.basename(path)
end
def uploader_class
Object.const_get(uploader)
def mount_point
super&.to_sym
end
end
......@@ -137,6 +137,7 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
#
......
class UserCallout < ActiveRecord::Base
belongs_to :user
enum feature_name: {
gke_cluster_integration: 1
}
validates :user, presence: true
validates :feature_name,
presence: true,
uniqueness: { scope: :user_id },
inclusion: { in: UserCallout.feature_names.keys }
end
class WikiPage
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
include ActiveModel::Validations
include ActiveModel::Conversion
......@@ -102,7 +103,7 @@ class WikiPage
# The hierarchy of the directory this page is contained in.
def directory
wiki.page_title_and_dir(slug).last
wiki.page_title_and_dir(slug)&.last.to_s
end
# The processed/formatted content of this page.
......@@ -177,7 +178,7 @@ class WikiPage
# Creates a new Wiki Page.
#
# attr - Hash of attributes to set on the new page.
# :title - The title for the new page.
# :title - The title (optionally including dir) for the new page.
# :content - The raw markup content.
# :format - Optional symbol representing the
# content format. Can be any type
......@@ -189,7 +190,7 @@ class WikiPage
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def create(attrs = {})
@attributes.merge!(attrs)
update_attributes(attrs)
save(page_details: title) do
wiki.create_page(title, content, format, message)
......@@ -204,24 +205,29 @@ class WikiPage
# See ProjectWiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
# :title - The Title to replace existing title
# :title - The Title (optionally including dir) to replace existing title
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha)
if last_commit_sha && last_commit_sha != self.last_commit_sha
raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
raise PageChangedError.new("WikiEdit|You are attempting to update a page that has changed since you started editing it.")
end
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
page_details =
if title.present? && @page.title != title
title
else
@page.url_path
update_attributes(attrs)
if title_changed?
page_details = title
if wiki.find_page(page_details).present?
@attributes[:title] = @page.url_path
raise PageRenameError.new("WikiEdit|There is already a page with the same title in that path.")
end
else
page_details = @page.url_path
end
save(page_details: page_details) do
wiki.update_page(
......@@ -255,8 +261,44 @@ class WikiPage
page.version.to_s
end
def title_changed?
title.present? && self.class.unhyphenize(@page.url_path) != title
end
private
# Process and format the title based on the user input.
def process_title(title)
return if title.blank?
title = deep_title_squish(title)
current_dirname = File.dirname(title)
if @page.present?
return title[1..-1] if current_dirname == '/'
return File.join([directory.presence, title].compact) if current_dirname == '.'
end
title
end
# This method squishes all the filename
# i.e: ' foo / bar / page_name' => 'foo/bar/page_name'
def deep_title_squish(title)
components = title.split(File::SEPARATOR).map(&:squish)
File.join(components)
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
......
......@@ -46,11 +46,11 @@ class FileMover
end
def uploader
@uploader ||= PersonalFileUploader.new(model, secret)
@uploader ||= PersonalFileUploader.new(model, secret: secret)
end
def temp_file_uploader
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret)
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
end
def revert
......
......@@ -62,9 +62,11 @@ class FileUploader < GitlabUploader
attr_accessor :model
def initialize(model, secret = nil)
def initialize(model, mounted_as = nil, **uploader_context)
super(model, nil, **uploader_context)
@model = model
@secret = secret
apply_context!(uploader_context)
end
def base_dir
......@@ -107,15 +109,17 @@ class FileUploader < GitlabUploader
self.file.filename
end
# the upload does not hold the secret, but holds the path
# which contains the secret: extract it
def upload=(value)
super
return unless value
return if apply_context!(value.uploader_context)
# fallback to the regex based extraction
if matches = DYNAMIC_PATH_PATTERN.match(value.path)
@secret = matches[:secret]
@identifier = matches[:identifier]
end
super
end
def secret
......@@ -124,6 +128,18 @@ class FileUploader < GitlabUploader
private
def apply_context!(uploader_context)
@secret, @identifier = uploader_context.values_at(:secret, :identifier)
!!(@secret && @identifier)
end
def build_upload
super.tap do |upload|
upload.secret = secret
end
end
def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end
......
......@@ -29,6 +29,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, :file_storage?, to: :class
def initialize(model, mounted_as = nil, **uploader_context)
super(model, mounted_as)
end
def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File)
end
......
......@@ -24,7 +24,7 @@ module RecordsUploads
uploads.where(path: upload_path).delete_all
upload.destroy! if upload
self.upload = build_upload_from_uploader(self)
self.upload = build_upload
upload.save!
end
end
......@@ -39,12 +39,13 @@ module RecordsUploads
Upload.order(id: :desc).where(uploader: self.class.to_s)
end
def build_upload_from_uploader(uploader)
def build_upload
Upload.new(
size: uploader.file.size,
path: uploader.upload_path,
model: uploader.model,
uploader: uploader.class.to_s
uploader: self.class.to_s,
size: file.size,
path: upload_path,
model: model,
mount_point: mounted_as
)
end
......
......@@ -9,7 +9,13 @@
.form-group
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12= f.text_field :title, class: 'form-control', value: @page.title
.col-sm-12
= f.text_field :title, class: 'form-control', value: @page.title
- if @page.persisted?
%span.edit-wiki-page-slug-tip
= icon('lightbulb-o')
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
......
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- page_title _("Edit"), @page.title.capitalize, _("Wiki")
- if @conflict
.alert.alert-danger
- page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
= (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
= wiki_page_errors(@error)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
......
---
title: Report the correct version and revision for Geo node status requests
merge_request: 4353
author:
type: fixed
---
title: Add Colors to GitLab Flavored Markdown
merge_request: 16095
author: Tony Rom <thetonyrom@gmail.com>
type: added
---
title: Show coverage to two decimal points in coverage badge
merge_request: 10083
author: Jeff Stubler
type: changed
---
title: Sanitize extra blank spaces used when uploading a SSH key
merge_request: 40552
author:
type: fixed
---
title: Fix export removal for hashed-storage projects within a renamed or deleted
namespace
merge_request: 16658
author:
type: fixed
---
title: Added uploader metadata to the uploads.
merge_request: 16779
author:
type: added
---
title: Fix forking projects when no restricted visibility levels are defined applicationwide
merge_request: 16881
author:
type: fixed
---
title: Allow moving wiki pages from the UI
merge_request: 16313
author:
type: fixed
---
title: Trigger change event on filename input when file template is applied
merge_request: 16911
author: Sebastian Klingler
type: fixed
---
title: Add backend for persistently dismissably callouts
merge_request:
author:
type: added
......@@ -193,12 +193,6 @@ production: &base
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...)
uploads:
# The location where uploads objects are stored (default: public/).
# storage_path: public/
# base_dir: uploads/-/system
## GitLab Pages
pages:
enabled: false
......
......@@ -35,6 +35,88 @@ module Gollum
[]
end
end
# Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
def update_page(page, name, format, data, commit = {})
name = name ? ::File.basename(name) : page.name
format ||= page.format
dir = ::File.dirname(page.path)
dir = '' if dir == '.'
filename = (rename = page.name != name) ? Gollum::Page.cname(name) : page.filename_stripped
multi_commit = !!commit[:committer]
committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
if !rename && page.format == format
committer.add(page.path, normalize(data))
else
committer.delete(page.path)
committer.add_to_index(dir, filename, format, data)
end
committer.after_commit do |index, _sha|
@access.refresh
index.update_working_dir(dir, page.filename_stripped, page.format)
index.update_working_dir(dir, filename, format)
end
multi_commit ? committer : committer.commit
end
# Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
def rename_page(page, rename, commit = {})
return false if page.nil?
return false if rename.nil? || rename.empty?
(target_dir, target_name) = ::File.split(rename)
(source_dir, source_name) = ::File.split(page.path)
source_name = page.filename_stripped
# File.split gives us relative paths with ".", commiter.add_to_index doesn't like that.
target_dir = '' if target_dir == '.'
source_dir = '' if source_dir == '.'
target_dir = target_dir.gsub(/^\//, '') # rubocop:disable Style/RegexpLiteral
# if the rename is a NOOP, abort
if source_dir == target_dir && source_name == target_name
return false
end
multi_commit = !!commit[:committer]
committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
# This piece only works for multi_commit
# If we are in a commit batch and one of the previous operations
# has updated the page, any information we ask to the page can be outdated.
# Therefore, we should ask first to the current committer tree to see if
# there is any updated change.
raw_data = raw_data_in_committer(committer, source_dir, page.filename) ||
raw_data_in_committer(committer, source_dir, "#{target_name}.#{Page.format_to_ext(page.format)}") ||
page.raw_data
committer.delete(page.path)
committer.add_to_index(target_dir, target_name, page.format, raw_data)
committer.after_commit do |index, _sha|
@access.refresh
index.update_working_dir(source_dir, source_name, page.format)
index.update_working_dir(target_dir, target_name, page.format)
end
multi_commit ? committer : committer.commit
end
# Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
def raw_data_in_committer(committer, dir, filename)
data = nil
[*dir.split(::File::SEPARATOR), filename].each do |key|
data = data ? data[key] : committer.tree[key]
break unless data
end
data
end
end
module Git
......
......@@ -75,6 +75,9 @@ Rails.application.routes.draw do
resources :issues, module: :boards, only: [:index, :update]
end
# UserCallouts
resources :user_callouts, only: [:create]
end
# Koding route
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateUserCallouts < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :user_callouts do |t|
t.integer :feature_name, null: false
t.references :user, index: true, foreign_key: { on_delete: :cascade }, null: false
end
add_index :user_callouts, [:user_id, :feature_name], unique: true
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddUploadsBuilderContext < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :uploads, :mount_point, :string
add_column :uploads, :secret, :string
end
end
# Copy of 20180202111106 - this one should run before 20171207150343 to fix issues related to
# the removal of groups with labels.
class RemoveProjectLabelsGroupIdCopy < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# rubocop:disable Migration/UpdateColumnInBatches
update_column_in_batches(:labels, :group_id, nil) do |table, query|
query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
end
# rubocop:enable Migration/UpdateColumnInBatches
end
def down
end
end
......@@ -999,6 +999,8 @@ ActiveRecord::Schema.define(version: 20180202111106) do
t.integer "job_artifacts_count"
t.integer "job_artifacts_synced_count"
t.integer "job_artifacts_failed_count"
t.string "version"
t.string "revision"
end
add_index "geo_node_statuses", ["geo_node_id"], name: "index_geo_node_statuses_on_geo_node_id", unique: true, using: :btree
......@@ -2253,6 +2255,8 @@ ActiveRecord::Schema.define(version: 20180202111106) do
t.string "uploader", null: false
t.datetime "created_at", null: false
t.integer "store"
t.string "mount_point"
t.string "secret"
end
add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
......@@ -2271,6 +2275,14 @@ ActiveRecord::Schema.define(version: 20180202111106) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
create_table "user_callouts", force: :cascade do |t|
t.integer "feature_name", null: false
t.integer "user_id", null: false
end
add_index "user_callouts", ["user_id", "feature_name"], name: "index_user_callouts_on_user_id_and_feature_name", unique: true, using: :btree
add_index "user_callouts", ["user_id"], name: "index_user_callouts_on_user_id", using: :btree
create_table "user_custom_attributes", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
......@@ -2604,6 +2616,7 @@ ActiveRecord::Schema.define(version: 20180202111106) do
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
add_foreign_key "user_callouts", "users", on_delete: :cascade
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
......
......@@ -13,7 +13,7 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
`GITLAB_CDN_HOST` | string | Sets the hostname for a CDN to serve static assets (e.g. `mycdnsubdomain.fictional-cdn.com`)
`GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (e.g. `//mycdnsubdomain.fictional-cdn.com`)
`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
......
......@@ -109,7 +109,7 @@ keys must be manually replicated to the secondary node.
1. Make a backup of any existing SSH host keys:
```bash
find /etc/ssh -iname ssh_host_* -exec mv {} {}.backup.`date +%F` \;
find /etc/ssh -iname ssh_host_* -exec cp {} {}.backup.`date +%F` \;
```
1. SSH into the **primary** node, and execute the command below:
......@@ -118,7 +118,7 @@ keys must be manually replicated to the secondary node.
sudo find /etc/ssh -iname ssh_host_* -not -iname '*.pub'
```
1. For each file in that list copy the file from the primary node to
1. For each file in that list replace the file from the primary node to
the **same** location on your **secondary** node.
1. On your **secondary** node, ensure the file permissions are correct:
......
......@@ -254,7 +254,7 @@ GFM will recognize the following:
| `@user_name` | specific user |
| `@group_name` | specific group |
| `@all` | entire team |
| `#123` | issue |
| `#12345` | issue |
| `!123` | merge request |
| `$123` | snippet |
| `&123` | epic |
......@@ -382,6 +382,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
### Colors
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#colors
It is possible to have color written in HEX, RGB or HSL format rendered with a color indicator.
Color written inside backticks will be followed by a color "chip".
Examples:
`#F00`
`#F00A`
`#FF0000`
`#FF0000AA`
`RGB(0,255,0)`
`RGB(0%,100%,0%)`
`RGBA(0,255,0,0.7)`
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
Becomes:
`#F00`
`#F00A`
`#FF0000`
`#FF0000AA`
`RGB(0,255,0)`
`RGB(0%,100%,0%)`
`RGBA(0,255,0,0.7)`
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
#### Supported formats:
* HEX: `` `#RGB[A]` `` or `` `#RRGGBB[AA]` ``
* RGB: `` `RGB[A](R, G, B[, A])` ``
* HSL: `` `HSL[A](H, S, L[, A])` ``
### Mermaid
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in
......
......@@ -46,7 +46,7 @@ namespace that started the import process.
The importer will also import branches on forks of projects related to open pull
requests. These branches will be imported with a naming scheme similar to
GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepancy
in branches compared to the GitHub Repository.
For a more technical description and an overview of the architecture you can
......
......@@ -64,6 +64,18 @@ effect.
You can find the **Delete** button only when editing a page. Click on it and
confirm you want the page to be deleted.
## Moving a wiki page
You can move a wiki page from one directory to another by specifying the full
path in the wiki page title in the [edit](#editing-a-wiki-page) form.
![Moving a page](img/wiki_move_page_1.png)
![After moving a page](img/wiki_move_page_2.png)
In order to move a wiki page to the root directory, the wiki page title must
be preceded by the slash (`/`) character.
## Viewing a list of all created wiki pages
Every wiki has a sidebar from which a short list of the created pages can be
......
module EE
module GeoHelper
def node_vue_list_properties
version, revision =
if ::Gitlab::Geo.primary?
[::Gitlab::VERSION, ::Gitlab::REVISION]
else
status = ::Gitlab::Geo.primary_node&.status
[status&.version, status&.revision]
end
{
primary_version: version.to_s,
primary_revision: revision.to_s,
node_details_path: admin_geo_nodes_path.to_s,
node_actions_allowed: ::Gitlab::Database.read_write?.to_s,
node_edit_allowed: ::Gitlab::Geo.license_allows?.to_s
}
end
def node_namespaces_options(namespaces)
namespaces.map { |g| { id: g.id, text: g.full_name } }
end
......
......@@ -4,7 +4,7 @@ class GeoNodeStatus < ActiveRecord::Base
delegate :selective_sync_type, to: :geo_node
# Whether we were successful in reaching this node
attr_accessor :success, :version, :revision
attr_accessor :success
attr_writer :health_status
attr_accessor :storage_shards
......@@ -58,10 +58,18 @@ class GeoNodeStatus < ActiveRecord::Base
GeoNodeStatus.new(HashWithIndifferentAccess.new(json_data))
end
EXCLUDED_PARAMS = %w[id created_at].freeze
EXTRA_PARAMS = %w[
success
health
health_status
last_event_timestamp
cursor_last_event_timestamp
storage_shards
].freeze
def self.allowed_params
excluded_params = %w(id created_at updated_at).freeze
extra_params = %w(success health health_status last_event_timestamp cursor_last_event_timestamp version revision storage_shards updated_at).freeze
self.column_names - excluded_params + extra_params
self.column_names - EXCLUDED_PARAMS + EXTRA_PARAMS
end
def load_data_from_current_node
......@@ -83,6 +91,9 @@ class GeoNodeStatus < ActiveRecord::Base
self.last_successful_status_check_at = Time.now
self.storage_shards = StorageShard.all
self.version = Gitlab::VERSION
self.revision = Gitlab::REVISION
load_primary_data
load_secondary_data
......
......@@ -37,8 +37,10 @@ module ObjectStorage
super
end
def build_upload_from_uploader(uploader)
super.tap { |upload| upload.store = object_store }
def build_upload
super.tap do |upload|
upload.store = object_store
end
end
def upload=(upload)
......
......@@ -23,6 +23,6 @@
.alert.alert-warning WARNING: Please upgrade PostgreSQL to version 9.6 or greater. The status of the replication cannot be determined reliably with the current version.
- if @nodes.any?
#js-geo-nodes{ data: { primary_version: "#{Gitlab::VERSION}", primary_revision: "#{Gitlab::REVISION}", node_details_path: "#{admin_geo_nodes_path}", node_actions_allowed: "#{Gitlab::Database.read_write?}", node_edit_allowed: "#{Gitlab::Geo.license_allows?}" } }
#js-geo-nodes{ data: node_vue_list_properties }
- else
= render 'shared/empty_states/geo'
class StoreVersionAndRevisionInGeoNodeStatus < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :geo_node_statuses, :version, :string
add_column :geo_node_statuses, :revision, :string
end
end
module EE
module API
module APIGuard
module HelperMethods
extend ::Gitlab::Utils::Override
override :find_user_from_sources
def find_user_from_sources
find_user_from_access_token ||
find_user_from_job_token ||
find_user_from_warden
end
end
end
end
end
......@@ -39,10 +39,11 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
prepend EE::API::APIGuard::HelperMethods
include Gitlab::Auth::UserAuthFinders
def find_current_user!
user = find_user_from_access_token || find_user_from_job_token || find_user_from_warden
user = find_user_from_sources
return unless user
forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
......@@ -50,6 +51,10 @@ module API
user
end
def find_user_from_sources
find_user_from_access_token || find_user_from_warden
end
private
# An array of scopes that were registered (using `allow_access_with_scope`)
......
......@@ -1267,14 +1267,6 @@ module API
def missing_oauth_application
object.geo_node.missing_oauth_application?
end
def version
Gitlab::VERSION
end
def revision
Gitlab::REVISION
end
end
class PersonalAccessToken < Grape::Entity
......
module Banzai
module ColorParser
ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/ # 0.0..1.0
PERCENTS = /(?:\d{1,2}|100)%/ # 00%..100%
ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/
BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/ # 00..255
DEGS = /-?\d+(?:deg)?/i # [-]digits[deg]
RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i # [-](digits[.digits] OR .digits)rad
HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/
RGB_FORMAT = %r{
(?:rgba?
\(
(?:
(?:(?:#{BITS},\s*){2}#{BITS})
|
(?:(?:#{PERCENTS},\s*){2}#{PERCENTS})
)
#{ALPHA_CHANNEL}
\)
)
}xi
HSL_FORMAT = %r{
(?:hsla?
\(
(?:#{DEGS}|#{RADS}),\s*#{PERCENTS},\s*#{PERCENTS}
#{ALPHA_CHANNEL}
\)
)
}xi
FORMATS = [HEX_FORMAT, RGB_FORMAT, HSL_FORMAT].freeze
COLOR_FORMAT = /\A(#{Regexp.union(FORMATS)})\z/ix
# Public: Analyzes whether the String is a color code.
#
# text - The String to be parsed.
#
# Returns the recognized color String or nil if none was found.
def self.parse(text)
text if COLOR_FORMAT =~ text
end
end
end
module Banzai
module Filter
# HTML filter that renders `color` followed by a color "chip".
#
class ColorFilter < HTML::Pipeline::Filter
COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze
def call
doc.css('code').each do |node|
color = ColorParser.parse(node.content)
node << color_chip(color) if color
end
doc
end
private
def color_chip(color)
checkerboard = doc.document.create_element('span', class: COLOR_CHIP_CLASS)
chip = doc.document.create_element('span', style: inline_styles(color: color))
checkerboard << chip
end
def inline_styles(color:)
"background-color: #{color};"
end
end
end
end
......@@ -7,6 +7,7 @@ module Banzai
Filter::SanitizationFilter,
Filter::EmojiFilter,
Filter::ColorFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter
]
......
......@@ -14,6 +14,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
Filter::ColorFilter,
Filter::MermaidFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
......
......@@ -23,7 +23,7 @@ module Gitlab
@coverage ||= raw_coverage
return unless @coverage
@coverage.to_i
@coverage.to_f.round(2)
end
def metadata
......
......@@ -25,7 +25,7 @@ module Gitlab
end
def value_text
@status ? "#{@status}%" : 'unknown'
@status ? ("%.2f%%" % @status) : 'unknown'
end
def key_width
......@@ -33,7 +33,7 @@ module Gitlab
end
def value_width
@status ? 36 : 58
@status ? 54 : 58
end
def value_color
......
......@@ -46,7 +46,7 @@ module Gitlab
private
def find_file(project, secret, file)
uploader = FileUploader.new(project, secret)
uploader = FileUploader.new(project, secret: secret)
uploader.retrieve_from_store!(file)
uploader.file
end
......
# Gitaly note: JV: no RPC's here.
module Gitlab
module Git
class Branch < Ref
......
......@@ -1343,20 +1343,23 @@ module Gitlab
raise CommandError.new(e)
end
def refs_contains_sha(ref_type, sha)
args = %W(#{ref_type} --contains #{sha})
names = run_git(args).first
if names.respond_to?(:split)
names = names.split("\n").map(&:strip)
names.each do |name|
name.slice! '* '
def branch_names_contains_sha(sha)
gitaly_migrate(:branch_names_contains_sha) do |is_enabled|
if is_enabled
gitaly_ref_client.branch_names_contains_sha(sha)
else
refs_contains_sha(:branch, sha)
end
end
end
names
else
[]
def tag_names_contains_sha(sha)
gitaly_migrate(:tag_names_contains_sha) do |is_enabled|
if is_enabled
gitaly_ref_client.tag_names_contains_sha(sha)
else
refs_contains_sha(:tag, sha)
end
end
end
......@@ -1434,6 +1437,21 @@ module Gitlab
end
end
def refs_contains_sha(ref_type, sha)
args = %W(#{ref_type} --contains #{sha})
names = run_git(args).first
return [] unless names.respond_to?(:split)
names = names.split("\n").map(&:strip)
names.each do |name|
name.slice! '* '
end
names
end
def rugged_write_config(full_path:)
rugged.config['gitlab.fullpath'] = full_path
end
......
# Gitaly note: JV: no RPC's here.
#
module Gitlab
module Git
class Tag < Ref
......
......@@ -25,8 +25,9 @@ module Gitlab
@repository.exists?
end
# Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539
def write_page(name, format, content, commit_details)
@repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
@repository.gitaly_migrate(:wiki_write_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_write_page(name, format, content, commit_details)
gollum_wiki.clear_cache
......@@ -47,8 +48,9 @@ module Gitlab
end
end
# Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094
def update_page(page_path, title, format, content, commit_details)
@repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
@repository.gitaly_migrate(:wiki_update_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details)
gollum_wiki.clear_cache
......@@ -68,8 +70,9 @@ module Gitlab
end
end
# Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039
def page(title:, version: nil, dir: nil)
@repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
@repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
......@@ -192,7 +195,10 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
gollum_wiki.write_page(name, format, content, commit_details.to_h)
filename = File.basename(name)
dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
nil
rescue Gollum::DuplicatePageError => e
......@@ -210,7 +216,15 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
page = gollum_page_by_path(page_path)
committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
# Instead of performing two renames if the title has changed,
# the update_page will only update the format and content and
# the rename_page will do anything related to moving/renaming
gollum_wiki.update_page(page, page.name, format, content, committer: committer)
gollum_wiki.rename_page(page, title, committer: committer)
committer.commit
nil
end
......
......@@ -145,6 +145,32 @@ module Gitlab
raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present?
end
# Limit: 0 implies no limit, thus all tag names will be returned
def tag_names_contains_sha(sha, limit: 0)
request = Gitaly::ListTagNamesContainingCommitRequest.new(
repository: @gitaly_repo,
commit_id: sha,
limit: limit
)
stream = GitalyClient.call(@repository.storage, :ref_service, :list_tag_names_containing_commit, request)
consume_ref_contains_sha_response(stream, :tag_names)
end
# Limit: 0 implies no limit, thus all tag names will be returned
def branch_names_contains_sha(sha, limit: 0)
request = Gitaly::ListBranchNamesContainingCommitRequest.new(
repository: @gitaly_repo,
commit_id: sha,
limit: limit
)
stream = GitalyClient.call(@repository.storage, :ref_service, :list_branch_names_containing_commit, request)
consume_ref_contains_sha_response(stream, :branch_names)
end
private
def consume_refs_response(response)
......@@ -215,6 +241,13 @@ module Gitlab
Gitlab::Git::Commit.decorate(@repository, hash)
end
def consume_ref_contains_sha_response(stream, collection_name)
stream.each_with_object([]) do |response, array|
encoded_names = response.send(collection_name).map { |b| Gitlab::Git.ref_name(b) } # rubocop:disable GitlabSecurity/PublicSend
array.concat(encoded_names)
end
end
def invalid_ref!(message)
raise Gitlab::Git::Repository::InvalidRef.new(message)
end
......
......@@ -21,6 +21,22 @@ module Gitlab
technology(name)&.supported_sizes
end
def self.sanitize(key_content)
ssh_type, *parts = key_content.strip.split
return key_content if parts.empty?
parts.each_with_object("#{ssh_type} ").with_index do |(part, content), index|
content << part
if Gitlab::SSHPublicKey.new(content).valid?
break [content, parts[index + 1]].compact.join(' ') # Add the comment part if present
elsif parts.size == index + 1 # return original content if we've reached the last element
break key_content
end
end
end
attr_reader :key_text, :key
# Unqualified MD5 fingerprint for compatibility
......@@ -37,23 +53,23 @@ module Gitlab
end
def valid?
key.present?
key.present? && bits && technology.supported_sizes.include?(bits)
end
def type
technology.name if valid?
technology.name if key.present?
end
def bits
return unless valid?
return if key.blank?
case type
when :rsa
key.n.num_bits
key.n&.num_bits
when :dsa
key.p.num_bits
key.p&.num_bits
when :ecdsa
key.group.order.num_bits
key.group.order&.num_bits
when :ed25519
256
else
......
......@@ -59,7 +59,7 @@ module Gitlab
def allowed_levels
restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels
self.values - restricted_levels
self.values - Array(restricted_levels)
end
def closest_allowed_level(target_level)
......
This diff is collapsed.
require 'spec_helper'
describe UserCalloutsController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe "POST #create" do
subject { post :create, feature_name: feature_name, format: :json }
context 'with valid feature name' do
let(:feature_name) { UserCallout.feature_names.keys.first }
context 'when callout entry does not exist' do
it 'should create a callout entry with dismissed state' do
expect { subject }.to change { UserCallout.count }.by(1)
end
it 'should return success' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when callout entry already exists' do
let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) }
it 'should return success' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'with invalid feature name' do
let(:feature_name) { 'bogus_feature_name' }
it 'should return bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
......@@ -12,7 +12,7 @@ describe GeoNodeStatus, :geo do
let!(:project_3) { create(:project) }
let!(:project_4) { create(:project) }
subject { described_class.current_node_status }
subject(:status) { described_class.current_node_status }
before do
stub_current_geo_node(secondary)
......@@ -425,6 +425,14 @@ describe GeoNodeStatus, :geo do
end
end
describe '#version' do
it { expect(status.version).to eq(Gitlab::VERSION) }
end
describe '#revision' do
it { expect(status.revision).to eq(Gitlab::REVISION) }
end
describe '#[]' do
it 'returns values for each attribute' do
expect(subject[:repositories_count]).to eq(4)
......
......@@ -25,6 +25,8 @@ FactoryBot.define do
cursor_last_event_id 1
cursor_last_event_timestamp { Time.now.to_i }
last_successful_status_check_timestamp { Time.now.beginning_of_day }
version { Gitlab::VERSION }
revision { Gitlab::REVISION }
end
trait :unhealthy do
......
......@@ -5,6 +5,10 @@ FactoryBot.define do
title
key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
factory :key_without_comment do
key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate }
end
factory :deploy_key, class: 'DeployKey'
factory :personal_key do
......
......@@ -125,6 +125,12 @@ FactoryBot.define do
avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :with_export do
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
end
trait :broken_storage do
after(:create) do |project|
project.update_column(:repository_storage, 'broken')
......
......@@ -3,26 +3,29 @@ FactoryBot.define do
model { build(:project) }
size 100.kilobytes
uploader "AvatarUploader"
mount_point :avatar
secret nil
store ObjectStorage::Store::LOCAL
# we should build a mount agnostic upload by default
transient do
mounted_as :avatar
secret SecureRandom.hex
filename 'myfile.jpg'
end
# this needs to comply with RecordsUpload::Concern#upload_path
path { File.join("uploads/-/system", model.class.to_s.underscore, mounted_as.to_s, 'avatar.jpg') }
path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') }
trait :personal_snippet_upload do
model { build(:personal_snippet) }
path { File.join(secret, 'myfile.jpg') }
path { File.join(secret, filename) }
uploader "PersonalFileUploader"
secret SecureRandom.hex
end
trait :issuable_upload do
path { File.join(secret, 'myfile.jpg') }
path { File.join(secret, filename) }
uploader "FileUploader"
secret SecureRandom.hex
end
trait :object_storage do
......@@ -31,13 +34,14 @@ FactoryBot.define do
trait :namespace_upload do
model { build(:group) }
path { File.join(secret, 'myfile.jpg') }
path { File.join(secret, filename) }
uploader "NamespaceFileUploader"
secret SecureRandom.hex
end
trait :attachment_upload do
transient do
mounted_as :attachment
mount_point :attachment
end
model { build(:note) }
......
FactoryBot.define do
factory :user_callout do
feature_name :gke_cluster_integration
user
end
end
......@@ -261,6 +261,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
it 'includes ColorFilter' do
expect(doc).to parse_colors
end
end
context 'wiki pipeline' do
......@@ -323,6 +327,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
it 'includes ColorFilter' do
expect(doc).to parse_colors
end
end
# Fake a `current_user` helper
......
......@@ -18,7 +18,7 @@ feature 'test coverage badge' do
show_test_coverage_badge
expect_coverage_badge('95%')
expect_coverage_badge('95.00%')
end
scenario 'user requests coverage badge for specific job' do
......@@ -30,7 +30,7 @@ feature 'test coverage badge' do
show_test_coverage_badge(job: 'coverage')
expect_coverage_badge('85%')
expect_coverage_badge('85.00%')
end
scenario 'user requests coverage badge for pipeline without coverage' do
......
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') }
let(:project) { create(:project) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
context 'admin user' do
shared_examples_for 'handling project exports on namespace change' do
let!(:old_export_path) { project.export_path }
before do
sign_in(create(:admin))
setup_export_project
end
context 'moving the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
it 'removes the export file' do
expect(File).to exist(old_export_path)
project.namespace.update(path: 'new_path')
project.namespace.update!(path: build(:namespace).path)
expect(File).not_to exist(old_export_path)
end
end
context 'deleting the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
it 'removes the export file' do
expect(File).to exist(old_export_path)
project.namespace.destroy
......@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(File).not_to exist(old_export_path)
end
end
end
def setup_export_project
visit edit_project_path(project)
describe 'legacy storage' do
let(:project) { create(:project) }
expect(page).to have_content('Export project')
it_behaves_like 'handling project exports on namespace change'
end
describe 'hashed storage' do
let(:project) { create(:project, :hashed) }
find(:link, 'Export project').send_keys(:return)
it_behaves_like 'handling project exports on namespace change'
end
visit edit_project_path(project)
def setup_export_project
visit edit_project_path(project)
expect(page).to have_content('Download export')
end
expect(page).to have_content('Export project')
find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
expect(page).to have_content('Download export')
end
end
require 'spec_helper'
describe 'User updates wiki page' do
# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
describe 'User updates wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
before do
......@@ -143,6 +144,7 @@ describe 'User updates wiki page' do
expect(page).to have_field('wiki[message]', with: 'Update home')
fill_in(:wiki_content, with: 'My awesome wiki!')
click_button('Save changes')
expect(page).to have_content('Home')
......@@ -151,4 +153,74 @@ describe 'User updates wiki page' do
end
end
end
context 'when the page is in a subdir' do
let!(:project) { create(:project, namespace: user.namespace) }
let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
let(:page_name) { 'page_name' }
let(:page_dir) { "foo/bar/#{page_name}" }
let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
before do
visit(project_wiki_edit_path(project, wiki_page))
end
it 'moves the page to the root folder' do
fill_in(:wiki_title, with: "/#{page_name}")
click_button('Save changes')
expect(current_path).to eq(project_wiki_path(project, page_name))
end
it 'moves the page to other dir' do
new_page_dir = "foo1/bar1/#{page_name}"
fill_in(:wiki_title, with: new_page_dir)
click_button('Save changes')
expect(current_path).to eq(project_wiki_path(project, new_page_dir))
end
it 'remains in the same place if title has not changed' do
original_path = project_wiki_path(project, wiki_page)
fill_in(:wiki_title, with: page_name)
click_button('Save changes')
expect(current_path).to eq(original_path)
end
it 'can be moved to a different dir with a different name' do
new_page_dir = "foo1/bar1/new_page_name"
fill_in(:wiki_title, with: new_page_dir)
click_button('Save changes')
expect(current_path).to eq(project_wiki_path(project, new_page_dir))
end
it 'can be renamed and moved to the root folder' do
new_name = 'new_page_name'
fill_in(:wiki_title, with: "/#{new_name}")
click_button('Save changes')
expect(current_path).to eq(project_wiki_path(project, new_name))
end
it 'squishes the title before creating the page' do
new_page_dir = " foo1 / bar1 / #{page_name} "
fill_in(:wiki_title, with: new_page_dir)
click_button('Save changes')
expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
end
end
end
require 'spec_helper'
describe 'User views a wiki page' do
# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
describe 'User views a wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do
......
......@@ -288,6 +288,18 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4)
### Colors
`#F00`
`#F00A`
`#FF0000`
`#FF0000AA`
`RGB(0,255,0)`
`RGB(0%,100%,0%)`
`RGBA(0,255,0,0.7)`
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
### Mermaid
> If this is not rendered correctly, see
......
require "spec_helper"
describe UserCalloutsHelper do
let(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe '.show_gke_cluster_integration_callout?' do
let(:project) { create(:project) }
subject { helper.show_gke_cluster_integration_callout?(project) }
context 'when user can create a cluster' do
before do
allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
.and_return(true)
end
context 'when user has not dismissed' do
before do
allow(helper).to receive(:user_dismissed?).and_return(false)
end
it { is_expected.to be true }
end
context 'when user dismissed' do
before do
allow(helper).to receive(:user_dismissed?).and_return(true)
end
it { is_expected.to be false }
end
end
context 'when user can not create a cluster' do
before do
allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
.and_return(false)
end
it { is_expected.to be false }
end
end
end
......@@ -130,16 +130,25 @@ describe('GfmAutoComplete', function () {
});
describe('should not match special sequences', () => {
const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
const shouldNotBePrependedBy = ['`'];
flagsUseDefaultMatcher.forEach((atSign) => {
ShouldNotBeFollowedBy.forEach((followedSymbol) => {
shouldNotBeFollowedBy.forEach((followedSymbol) => {
const seq = atSign + followedSymbol;
it(`should not match "${seq}"`, () => {
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
});
});
shouldNotBePrependedBy.forEach((prependedSymbol) => {
const seq = prependedSymbol + atSign;
it(`should not match "${seq}"`, () => {
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
});
});
});
});
});
......
require 'spec_helper'
describe Banzai::ColorParser do
describe '.parse' do
context 'HEX format' do
[
'#abc', '#ABC',
'#d2d2d2', '#D2D2D2',
'#123a', '#123A',
'#123456aa', '#123456AA'
].each do |color|
it "parses the valid hex color #{color}" do
expect(subject.parse(color)).to eq(color)
end
end
[
'#', '#1', '#12', '#12g', '#12G',
'#12345', '#r2r2r2', '#R2R2R2', '#1234567',
'# 123', '# 1234', '# 123456', '# 12345678',
'#1 2 3', '#123 4', '#12 34 56', '#123456 78'
].each do |color|
it "does not parse the invalid hex color #{color}" do
expect(subject.parse(color)).to be_nil
end
end
end
context 'RGB format' do
[
'rgb(0,0,0)', 'rgb(255,255,255)',
'rgb(0, 0, 0)', 'RGB(0,0,0)',
'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)',
'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)',
'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)',
'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)',
'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)'
].each do |color|
it "parses the valid rgb color #{color}" do
expect(subject.parse(color)).to eq(color)
end
end
[
'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR',
'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)',
'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)',
'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)',
'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)',
'rgb(0,0,0%)', 'rgb(101%,0%,0%)'
].each do |color|
it "does not parse the invalid rgb color #{color}" do
expect(subject.parse(color)).to be_nil
end
end
end
context 'HSL format' do
[
'hsl(0,0%,0%)', 'hsl(0,100%,100%)',
'hsl(540,0%,0%)', 'hsl(-720,0%,0%)',
'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)',
'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)',
'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)',
'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)',
'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)',
'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)',
'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)',
'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)'
].each do |color|
it "parses the valid hsl color #{color}" do
expect(subject.parse(color)).to eq(color)
end
end
[
'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)',
'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)',
'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)',
'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)',
'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)',
'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)'
].each do |color|
it "does not parse the invalid hsl color #{color}" do
expect(subject.parse(color)).to be_nil
end
end
end
end
end
require 'spec_helper'
describe Banzai::Filter::ColorFilter, lib: true do
include FilterSpecHelper
let(:color) { '#F00' }
let(:color_chip_selector) { 'code > span.gfm-color_chip > span' }
['#123', '#1234', '#123456', '#12345678',
'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)',
'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color|
it "inserts color chip for supported color format #{color}" do
content = code_tag(color)
doc = filter(content)
color_chip = doc.at_css(color_chip_selector)
expect(color_chip.content).to be_empty
expect(color_chip.parent[:class]).to eq 'gfm-color_chip'
expect(color_chip[:style]).to eq "background-color: #{color};"
end
end
it 'ignores valid color code without backticks(code tags)' do
doc = filter(color)
expect(doc.css('span.gfm-color_chip').size).to be_zero
end
it 'ignores valid color code with prepended space' do
content = code_tag(' ' + color)
doc = filter(content)
expect(doc.css(color_chip_selector).size).to be_zero
end
it 'ignores valid color code with appended space' do
content = code_tag(color + ' ')
doc = filter(content)
expect(doc.css(color_chip_selector).size).to be_zero
end
it 'ignores valid color code surrounded by spaces' do
content = code_tag(' ' + color + ' ')
doc = filter(content)
expect(doc.css(color_chip_selector).size).to be_zero
end
it 'ignores invalid color code' do
invalid_color = '#BAR'
content = code_tag(invalid_color)
doc = filter(content)
expect(doc.css(color_chip_selector).size).to be_zero
end
def code_tag(string)
"<code>#{string}</code>"
end
end
......@@ -2,8 +2,8 @@ require 'spec_helper'
describe FileSizeValidator do
let(:validator) { described_class.new(options) }
let(:attachment) { AttachmentUploader.new }
let(:note) { create(:note) }
let(:attachment) { AttachmentUploader.new(note) }
describe 'options uses an integer' do
let(:options) { { maximum: 10, attributes: { attachment: attachment } } }
......
require 'spec_helper'
describe Gitlab::Badge::Coverage::Template do
let(:badge) { double(entity: 'coverage', status: 90) }
let(:badge) { double(entity: 'coverage', status: 90.00) }
let(:template) { described_class.new(badge) }
describe '#key_text' do
......@@ -13,7 +13,17 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_text' do
context 'when coverage is known' do
it 'returns coverage percentage' do
expect(template.value_text).to eq '90%'
expect(template.value_text).to eq '90.00%'
end
end
context 'when coverage is known to many digits' do
before do
allow(badge).to receive(:status).and_return(92.349)
end
it 'returns rounded coverage percentage' do
expect(template.value_text).to eq '92.35%'
end
end
......@@ -37,7 +47,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_width' do
context 'when coverage is known' do
it 'is narrower when coverage is known' do
expect(template.value_width).to eq 36
expect(template.value_width).to eq 54
end
end
......@@ -113,7 +123,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#width' do
context 'when coverage is known' do
it 'returns the key width plus value width' do
expect(template.width).to eq 98
expect(template.width).to eq 116
end
end
......
......@@ -1162,14 +1162,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
it 'should reload Rugged::Repository and return master' do
expect(Rugged::Repository).to receive(:new).twice.and_call_original
context 'force_reload is true' do
it 'should reload Rugged::Repository' do
expect(Rugged::Repository).to receive(:new).twice.and_call_original
repository.find_branch('master')
branch = repository.find_branch('master', force_reload: true)
repository.find_branch('master')
branch = repository.find_branch('master', force_reload: true)
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
end
context 'force_reload is false' do
it 'should not reload Rugged::Repository' do
expect(Rugged::Repository).to receive(:new).once.and_call_original
branch = repository.find_branch('master', force_reload: false)
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
end
end
end
......
require 'spec_helper'
describe Gitlab::Git::Wiki do
let(:project) { create(:project) }
let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) }
let(:gollum_wiki) { wiki.wiki }
# Remove skip_gitaly_mock flag when gitaly_find_page when
# https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged
describe '#page', :skip_gitaly_mock do
it 'returns the right page' do
create_page('page1', 'content')
create_page('foo/page1', 'content')
expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1'
expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
destroy_page('page1')
destroy_page('page1', 'foo')
end
end
def create_page(name, content)
gollum_wiki.write_page(name, :markdown, content, commit_details)
end
def commit_details
Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
end
def destroy_page(title, dir = '')
page = gollum_wiki.page(title: title, dir: dir)
wiki.delete_page(page, "test commit")
end
end
......@@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do
end
end
describe '.sanitize(key_content)' do
let(:content) { build(:key).key }
context 'when key has blank space characters' do
it 'removes the extra blank space characters' do
unsanitized = content.insert(100, "\n")
.insert(40, "\r\n")
.insert(30, ' ')
sanitized = described_class.sanitize(unsanitized)
_, body = sanitized.split
expect(sanitized).not_to eq(unsanitized)
expect(body).not_to match(/\s/)
end
end
context "when key doesn't have blank space characters" do
it "doesn't modify the content" do
sanitized = described_class.sanitize(content)
expect(sanitized).to eq(content)
end
end
context "when key is invalid" do
it 'returns the original content' do
unsanitized = "ssh-foo any content=="
sanitized = described_class.sanitize(unsanitized)
expect(sanitized).to eq(unsanitized)
end
end
end
describe '#valid?' do
subject { public_key }
......
......@@ -57,6 +57,15 @@ describe Gitlab::VisibilityLevel do
expect(described_class.allowed_levels)
.to contain_exactly(described_class::PRIVATE, described_class::PUBLIC)
end
it 'returns all levels when no visibility level was set' do
allow(described_class)
.to receive_message_chain('current_application_settings.restricted_visibility_levels')
.and_return(nil)
expect(described_class.allowed_levels)
.to contain_exactly(described_class::PRIVATE, described_class::INTERNAL, described_class::PUBLIC)
end
end
describe '.closest_allowed_level' do
......
......@@ -72,16 +72,53 @@ describe Key, :mailer do
expect(build(:key)).to be_valid
end
it 'accepts a key with newline charecters after stripping them' do
key = build(:key)
key.key = key.key.insert(100, "\n")
key.key = key.key.insert(40, "\r\n")
expect(key).to be_valid
end
it 'rejects the unfingerprintable key (not a key)' do
expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid
end
where(:factory, :chars, :expected_sections) do
[
[:key, ["\n", "\r\n"], 3],
[:key, [' ', ' '], 3],
[:key_without_comment, [' ', ' '], 2]
]
end
with_them do
let!(:key) { create(factory) }
let!(:original_fingerprint) { key.fingerprint }
it 'accepts a key with blank space characters after stripping them' do
modified_key = key.key.insert(100, chars.first).insert(40, chars.last)
_, content = modified_key.split
key.update!(key: modified_key)
expect(key).to be_valid
expect(key.key.split.size).to eq(expected_sections)
expect(content).not_to match(/\s/)
expect(original_fingerprint).to eq(key.fingerprint)
end
end
end
context 'validate size' do
where(:key_content, :result) do
[
[Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false],
[Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false],
[Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true]
]
end
with_them do
it 'validates the size of the key' do
key = build(:key, key: key_content)
expect(key.valid?).to eq(result)
end
end
end
context 'validate it meets key restrictions' do
......
......@@ -733,4 +733,24 @@ describe Namespace do
end
end
end
describe '#remove_exports' do
let(:legacy_project) { create(:project, :with_export, namespace: namespace) }
let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) }
let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') }
let(:legacy_export) { legacy_project.export_project_path }
let(:hashed_export) { hashed_project.export_project_path }
it 'removes exports for legacy and hashed projects' do
allow(Gitlab::ImportExport).to receive(:storage_path) { export_path }
expect(File.exist?(legacy_export)).to be_truthy
expect(File.exist?(hashed_export)).to be_truthy
namespace.remove_exports!
expect(File.exist?(legacy_export)).to be_falsy
expect(File.exist?(hashed_export)).to be_falsy
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment