Commit e646439e authored by Ruben Davila's avatar Ruben Davila

Merge commit 'dev/8-12-stable' into 8-12-stable

parents 467e1ca4 ba8aeb7c
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.12.4 (unreleased) v 8.12.5 (unreleased)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
...@@ -8,6 +22,7 @@ v 8.12.3 ...@@ -8,6 +22,7 @@ v 8.12.3
v 8.12.2 v 8.12.2
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination - Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
- Fix List-Unsubscribe header in emails - Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes - Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513 - Fix an issue with the "Commits" section of the cycle analytics summary. !6513
......
...@@ -23,8 +23,9 @@ ...@@ -23,8 +23,9 @@
selectable: true, selectable: true,
filterable: true, filterable: true,
filterByText: true, filterByText: true,
fieldName: $dropdown.attr('name'), toggleLabel: true,
filterInput: 'input[type="text"]', fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) { renderRow: function(ref) {
var link; var link;
if (ref.header != null) { if (ref.header != null) {
......
...@@ -26,15 +26,15 @@ ...@@ -26,15 +26,15 @@
}; };
showTooltip = function(target, title) { showTooltip = function(target, title) {
return $(target).tooltip({ var $target = $(target);
container: 'body', var originalTitle = $target.data('original-title');
html: 'true',
placement: 'auto bottom', $target
title: title, .attr('title', 'Copied!')
trigger: 'manual' .tooltip('fixTitle')
}).tooltip('show').one('mouseleave', function() { .tooltip('show')
return $(this).tooltip('hide'); .attr('title', originalTitle)
}); .tooltip('fixTitle');
}; };
$(function() { $(function() {
......
...@@ -107,10 +107,14 @@ ...@@ -107,10 +107,14 @@
.block { .block {
width: 100%; width: 100%;
}
.block-first { &.coverage {
padding: 5px 16px 11px; padding: 0 16px 11px;
}
.btn-group-justified {
margin-top: 5px;
}
} }
.js-build-variable { .js-build-variable {
...@@ -214,6 +218,9 @@ ...@@ -214,6 +218,9 @@
.build-detail-row { .build-detail-row {
margin-bottom: 5px; margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
} }
.build-light-text { .build-light-text {
......
...@@ -743,6 +743,62 @@ pre.light-well { ...@@ -743,6 +743,62 @@ pre.light-well {
.dropdown-menu { .dropdown-menu {
width: 300px; width: 300px;
} }
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
} }
.clearable-input { .clearable-input {
...@@ -779,4 +835,4 @@ pre.light-well { ...@@ -779,4 +835,4 @@ pre.light-well {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
\ No newline at end of file
...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor ...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor ...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor ...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -507,9 +507,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -507,9 +507,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately. # running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author) def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do transaction do
self.merge_requests_closing_issues.delete_all self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue| closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue) self.merge_requests_closing_issues.create!(issue: issue)
end end
......
...@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base ...@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds) FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project # Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false
......
...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base ...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end end
def #{arg}=(value) def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value self.properties['#{arg}'] = value
end end
......
...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base ...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true) todos_pending_count(force: true)
end end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private private
def projects_union(min_access_level = nil) def projects_union(min_access_level = nil)
......
...@@ -7,6 +7,8 @@ module Projects ...@@ -7,6 +7,8 @@ module Projects
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # Make sure that the user is allowed to use the specified visibility level
...@@ -92,7 +94,7 @@ module Projects ...@@ -92,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.create_wiki if @project.feature_available?(:wiki, current_user) @project.create_wiki unless skip_wiki?
@project.build_missing_services @project.build_missing_services
@project.create_labels @project.create_labels
...@@ -106,6 +108,10 @@ module Projects ...@@ -106,6 +108,10 @@ module Projects
end end
end end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data) def save_project_and_import_data(import_data)
Project.transaction do Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right') = icon('angle-double-right')
- if @build.coverage - if @build.coverage
.block.block-first .block.coverage
.title .title
Test coverage Test coverage
%p.build-detail-row %p.build-detail-row
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
- @build.trigger_request.variables.each do |key, value| - @build.trigger_request.variables.each do |key, value|
.hide.js-build .hide.js-build
.js-build-variable= key .js-build-variable= key
.js-build-value= value .js-build-value= value
.block .block
......
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix .clearfix
- if params[:to] && params[:from] - if params[:to] && params[:from]
= link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} .compare-switch-container
.form-group.dropdown.compare-form-group.js-compare-from-dropdown = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group .input-group.inline-input-group
%span.input-group-addon from %span.input-group-addon from
= text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence } = hidden_field_tag :from, params[:from]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown" = render "ref_dropdown"
= "..." .compare-ellipsis ...
.form-group.dropdown.compare-form-group.js-compare-to-dropdown .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group .input-group.inline-input-group
%span.input-group-addon to %span.input-group-addon to
= text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence } = hidden_field_tag :to, params[:to]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text= params[:to] || 'Select branch/tag'
= render "ref_dropdown" = render "ref_dropdown"
&nbsp; &nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn" = button_tag "Compare", class: "btn btn-create commits-compare-btn"
......
.dropdown-menu.dropdown-menu-selectable .dropdown-menu.dropdown-menu-selectable
= dropdown_title "Select branch/tag" = dropdown_title "Select branch/tag"
= dropdown_filter "Filter by branch/tag"
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
...@@ -165,10 +165,9 @@ module Gitlab ...@@ -165,10 +165,9 @@ module Gitlab
end end
def import_wiki def import_wiki
unless project.wiki_enabled? unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project) wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end end
rescue Gitlab::Shell::Error => e rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created, # GitHub error message when the wiki repo has not been created,
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class ProjectCreator class ProjectCreator
attr_reader :repo, :namespace, :current_user, :session_data attr_reader :repo, :name, :namespace, :current_user, :session_data
def initialize(repo, name, namespace, current_user, session_data) def initialize(repo, name, namespace, current_user, session_data)
@repo = repo @repo = repo
...@@ -12,24 +12,37 @@ module Gitlab ...@@ -12,24 +12,37 @@ module Gitlab
end end
def execute def execute
project = ::Projects::CreateService.new( ::Projects::CreateService.new(
current_user, current_user,
name: @name, name: name,
path: @name, path: name,
description: repo.description, description: repo.description,
namespace_id: namespace.id, namespace_id: namespace.id,
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility, visibility_level: visibility_level,
import_type: "github", import_type: "github",
import_source: repo.full_name, import_source: repo.full_name,
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") import_url: import_url,
skip_wiki: skip_wiki
).execute ).execute
end
private
# If repo has wiki we'll import it later def import_url
if repo.has_wiki? && project repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) end
end
def visibility_level
repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
end
project #
# If the GitHub project repository has wiki, we should not create the
# default wiki. Otherwise the GitHub importer will fail because the wiki
# repository already exist.
#
def skip_wiki
repo.has_wiki?
end end
end end
end end
......
module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value|
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
end
end
end
end
module Gitlab module Gitlab
module ImportExport module ImportExport
module CommandLineUtil module CommandLineUtil
DEFAULT_MODE = 0700
def tar_czf(archive:, dir:) def tar_czf(archive:, dir:)
tar_with_options(archive: archive, dir: dir, options: 'czf') tar_with_options(archive: archive, dir: dir, options: 'czf')
end end
...@@ -21,6 +23,11 @@ module Gitlab ...@@ -21,6 +23,11 @@ module Gitlab
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end end
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
end
private private
def tar_with_options(archive:, dir:, options:) def tar_with_options(archive:, dir:, options:)
...@@ -45,7 +52,7 @@ module Gitlab ...@@ -45,7 +52,7 @@ module Gitlab
# if we are copying files, create the destination folder # if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination destination_folder = File.file?(source) ? File.dirname(destination) : destination
FileUtils.mkdir_p(destination_folder) mkdir_p(destination_folder)
FileUtils.copy_entry(source, destination) FileUtils.copy_entry(source, destination)
true true
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
end end
def import def import
FileUtils.mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
wait_for_archived_file do wait_for_archived_file do
decompress_archive decompress_archive
......
...@@ -110,9 +110,10 @@ module Gitlab ...@@ -110,9 +110,10 @@ module Gitlab
def create_relation(relation, relation_hash_list) def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash| relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: relation_hash.merge('project_id' => restored_project.id), relation_hash: relation_hash,
members_mapper: members_mapper, members_mapper: members_mapper,
user: @user) user: @user,
project_id: restored_project.id)
end end
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class ProjectTreeSaver class ProjectTreeSaver
include Gitlab::ImportExport::CommandLineUtil
attr_reader :full_path attr_reader :full_path
def initialize(project:, shared:) def initialize(project:, shared:)
...@@ -10,7 +12,7 @@ module Gitlab ...@@ -10,7 +12,7 @@ module Gitlab
end end
def save def save
FileUtils.mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree) File.write(full_path, project_json_tree)
true true
......
...@@ -13,6 +13,8 @@ module Gitlab ...@@ -13,6 +13,8 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze BUILD_MODELS = %w[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
...@@ -25,9 +27,9 @@ module Gitlab ...@@ -25,9 +27,9 @@ module Gitlab
new(*args).create new(*args).create
end end
def initialize(relation_sym:, relation_hash:, members_mapper:, user:) def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym @relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_hash = relation_hash.except('id', 'noteable_id') @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper @members_mapper = members_mapper
@user = user @user = user
@imported_object_retries = 0 @imported_object_retries = 0
...@@ -153,7 +155,11 @@ module Gitlab ...@@ -153,7 +155,11 @@ module Gitlab
end end
def parsed_relation_hash def parsed_relation_hash
@parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) } @parsed_relation_hash ||= begin
Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
@relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
end end
def set_st_diffs def set_st_diffs
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
def restore def restore
return true unless File.exist?(@path_to_bundle) return true unless File.exist?(@path_to_bundle)
FileUtils.mkdir_p(path_to_repo) mkdir_p(path_to_repo)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e rescue => e
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
private private
def bundle_to_disk def bundle_to_disk
FileUtils.mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
rescue => e rescue => e
@shared.error(e) @shared.error(e)
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class VersionSaver class VersionSaver
include Gitlab::ImportExport::CommandLineUtil
def initialize(shared:) def initialize(shared:)
@shared = shared @shared = shared
end end
def save def save
FileUtils.mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
File.write(version_file, Gitlab::ImportExport.version, mode: 'w') File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
rescue => e rescue => e
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
end end
def bundle_to_disk(full_path) def bundle_to_disk(full_path)
FileUtils.mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path) git_bundle(repo_path: path_to_repo, bundle_path: full_path)
rescue => e rescue => e
@shared.error(e) @shared.error(e)
......
...@@ -20,13 +20,8 @@ module Gitlab ...@@ -20,13 +20,8 @@ module Gitlab
def token def token
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
token = redis.get(redis_key) token = redis.get(redis_key)
token ||= Devise.friendly_token(TOKEN_LENGTH)
if token redis.set(redis_key, token, ex: EXPIRY_TIME)
redis.expire(redis_key, EXPIRY_TIME)
else
token = Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
end
token token
end end
......
...@@ -109,6 +109,44 @@ describe SessionsController do ...@@ -109,6 +109,44 @@ describe SessionsController do
end end
end end
context 'when the user is on their last attempt' do
before do
user.update(failed_attempts: User.maximum_attempts.pred)
end
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid login' do
expect(response).to set_flash.now[:alert]
.to /Invalid Login or password/
end
it 'locks the user' do
expect(user.reload).to be_access_locked
end
it 'keeps the user locked on future login attempts' do
post(:create, user: { login: user.username, password: user.password })
expect(response)
.to set_flash.now[:alert].to /Invalid Login or password/
end
end
end
context 'when another user does not have 2FA enabled' do context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) } let(:another_user) { create(:user) }
......
...@@ -12,15 +12,16 @@ describe "Compare", js: true do ...@@ -12,15 +12,16 @@ describe "Compare", js: true do
describe "branches" do describe "branches" do
it "pre-populates fields" do it "pre-populates fields" do
expect(page.find_field("from").value).to eq("master") expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end end
it "compares branches" do it "compares branches" do
fill_in "from", with: "fea" select_using_dropdown "from", "feature"
find("#from").click expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
click_link "feature" select_using_dropdown "to", "binary-encoding"
expect(page.find_field("from").value).to eq("feature") expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
click_button "Compare" click_button "Compare"
expect(page).to have_content "Commits" expect(page).to have_content "Commits"
...@@ -29,14 +30,21 @@ describe "Compare", js: true do ...@@ -29,14 +30,21 @@ describe "Compare", js: true do
describe "tags" do describe "tags" do
it "compares tags" do it "compares tags" do
fill_in "from", with: "v1.0" select_using_dropdown "from", "v1.0.0"
find("#from").click expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
click_link "v1.0.0" select_using_dropdown "to", "v1.1.0"
expect(page.find_field("from").value).to eq("v1.0.0") expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
click_button "Compare" click_button "Compare"
expect(page).to have_content "Commits" expect(page).to have_content "Commits"
end end
end end
def select_using_dropdown(dropdown_type, selection)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
dropdown.fill_in("Filter by branch/tag", with: selection)
click_link selection
end
end end
...@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr ...@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
expect(page).to have_content('Download export') expect(page).to have_content('Download export')
expect(file_permissions(project.export_path)).to eq(0700)
in_directory_with_expanded_export(project) do |exit_status, tmpdir| in_directory_with_expanded_export(project) do |exit_status, tmpdir|
expect(exit_status).to eq(0) expect(exit_status).to eq(0)
......
...@@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do ...@@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil) expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
end end
context 'when Github project is private' do context 'when GitHub project is private' do
it 'sets project visibility to private' do it 'sets project visibility to private' do
repo.private = true repo.private = true
...@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do ...@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
end end
end end
context 'when Github project is public' do context 'when GitHub project is public' do
before do before do
allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
end end
...@@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do ...@@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end end
end end
context 'when GitHub project has wiki' do
it 'does not create the wiki repository' do
allow(repo).to receive(:has_wiki?).and_return(true)
project = service.execute
expect(project.wiki.repository_exists?).to eq false
end
end
context 'when GitHub project does not have wiki' do
it 'creates the wiki repository' do
allow(repo).to receive(:has_wiki?).and_return(false)
project = service.execute
expect(project.wiki.repository_exists?).to eq true
end
end
end end
end end
require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner, lib: true do
let(:unsafe_hash) do
{
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
'ci_id' => 99,
'random_project_id' => 99,
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
'user_id' => 99,
'random_id_in_the_middle' => 99,
'notid' => 99
}
end
let(:post_safe_hash) do
{
'project_id' => 99,
'user_id' => 99,
'random_id_in_the_middle' => 99,
'notid' => 99
}
end
it 'removes unwanted attributes from the hash' do
described_class.clean!(relation_hash: unsafe_hash)
expect(unsafe_hash).to eq(post_safe_hash)
end
end
require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory, lib: true do
let(:project) { create(:empty_project) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:user) { create(:user) }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
members_mapper: members_mapper,
user: user,
project_id: project.id)
end
context 'hook object' do
let(:relation_sym) { :hooks }
let(:id) { 999 }
let(:service_id) { 99 }
let(:original_project_id) { 8 }
let(:token) { 'secret' }
let(:relation_hash) do
{
'id' => id,
'url' => 'https://example.json',
'project_id' => original_project_id,
'created_at' => '2016-08-12T09:41:03.462Z',
'updated_at' => '2016-08-12T09:41:03.462Z',
'service_id' => service_id,
'push_events' => true,
'issues_events' => false,
'merge_requests_events' => true,
'tag_push_events' => false,
'note_events' => true,
'enable_ssl_verification' => true,
'build_events' => false,
'wiki_page_events' => true,
'token' => token
}
end
it 'does not have the original ID' do
expect(created_object.id).not_to eq(id)
end
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
end
it 'does not have the original project_id' do
expect(created_object.project_id).not_to eq(original_project_id)
end
it 'has the new project_id' do
expect(created_object.project_id).to eq(project.id)
end
it 'has a token' do
expect(created_object.token).to eq(token)
end
context 'original service exists' do
let(:service_id) { Service.create(project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
end
end
end
# Mocks an ActiveRecordish object with the dodgy columns
class FooModel
include ActiveModel::Model
def initialize(params)
params.each { |key, value| send("#{key}=", value) }
end
def values
instance_variables.map { |ivar| instance_variable_get(ivar) }
end
end
# `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
# re-assigned by described_class.
context 'Potentially hazardous foreign keys' do
let(:relation_sym) { :hazardous_foo_model }
let(:relation_hash) do
{
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
'ci_id' => 99,
'random_project_id' => 99,
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
'user_id' => 99,
}
end
class HazardousFooModel < FooModel
attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
end
it 'does not preserve any foreign key IDs' do
expect(created_object.values).not_to include(99)
end
end
context 'Project references' do
let(:relation_sym) { :project_foo_model }
let(:relation_hash) do
Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
end
class ProjectFooModel < FooModel
attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
end
it 'does not preserve any project foreign key IDs' do
expect(created_object.values).not_to include(99)
end
end
end
...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do ...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end end
end end
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
subject.target_branch = subject.project.default_branch
end
it 'caches closed issues' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
subject.project.update_attribute(:has_external_issue_tracker, true)
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
describe '#source_branch_sha' do describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
......
...@@ -203,6 +203,23 @@ describe Service, models: true do ...@@ -203,6 +203,23 @@ describe Service, models: true do
end end
end end
describe 'initialize service with no properties' do
let(:service) do
GitlabIssueTrackerService.create(
project: create(:project),
title: 'random title'
)
end
it 'does not raise error' do
expect { service }.not_to raise_error
end
it 'creates the properties' do
expect(service.properties).to eq({ "title" => "random title" })
end
end
describe "callbacks" do describe "callbacks" do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:service) do let!(:service) do
......
...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do ...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do
end end
end end
context 'closes related issues' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
end
it 'closes GitLab issue tracker issues' do
issue = create :issue, project: project
commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
service.execute(merge_request)
expect(issue.reload.closed?).to be_truthy
end
context 'with JIRA integration' do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
before { jira_service_settings }
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
service.execute(merge_request)
end
end
end
context 'remove source branch by author' do context 'remove source branch by author' do
let(:service) do let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1' merge_request.merge_params['force_remove_source_branch'] = '1'
......
...@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do ...@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do
let!(:project) { create(:project, namespace: user.namespace) } let!(:project) { create(:project, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo } let!(:path) { project.repository.path_to_repo }
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
context 'Sidekiq inline' do context 'Sidekiq inline' do
before do before do
...@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do ...@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy } it { expect(Dir.exist?(remove_path)).to be_truthy }
end end
context 'async delete of project with private issue visibility' do
let!(:async) { true }
before do
project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
# Run sidekiq immediately to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
it 'deletes the project' do
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
expect(Dir.exist?(remove_path)).to be_falsey
end
end
context 'container registry' do context 'container registry' do
before do before do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
...@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do ...@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do
end end
def destroy_project(project, user, params) def destroy_project(project, user, params)
Projects::DestroyService.new(project, user, params).execute if async
Projects::DestroyService.new(project, user, params).async_execute
else
Projects::DestroyService.new(project, user, params).execute
end
end end
end end
...@@ -130,4 +130,8 @@ module ExportFileHelper ...@@ -130,4 +130,8 @@ module ExportFileHelper
(parsed_model_attributes - parent.keys - excluded_attributes).empty? (parsed_model_attributes - parent.keys - excluded_attributes).empty?
end end
def file_permissions(file)
File.stat(file).mode & 0777
end
end end
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