Commit d5e5eb1d authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-01-03

# Conflicts:
#	app/assets/javascripts/init_legacy_filters.js
#	app/assets/javascripts/milestone_select.js
#	app/models/project_team.rb
#	doc/user/project/quick_actions.md
#	features/steps/shared/project.rb
#	qa/qa/page/menu/admin.rb
#	spec/controllers/projects/imports_controller_spec.rb
#	spec/features/issues/form_spec.rb
#	spec/features/merge_requests/edit_mr_spec.rb
#	spec/lib/gitlab/reference_extractor_spec.rb
#	spec/requests/api/issues_spec.rb
#	spec/services/git_push_service_spec.rb
#	spec/services/quick_actions/interpret_service_spec.rb

[ci skip]
parents a92be4e3 5e310367
......@@ -625,6 +625,14 @@ codequality:
artifacts:
paths: [codeclimate.json]
sast:
image: registry.gitlab.com/gitlab-org/gl-sast:latest
before_script: []
script:
- /app/bin/run .
artifacts:
paths: [gl-sast-report.json]
qa:internal:
<<: *dedicated-runner
<<: *except-docs
......
......@@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0'
# Supported DBs
gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0'
......@@ -295,7 +295,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'raindrops', '~> 0.18'
end
......@@ -418,7 +418,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.61.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.62.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -308,7 +308,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.61.0)
gitaly-proto (0.62.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -533,7 +533,7 @@ GEM
mustermann (1.0.0)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.5)
mysql2 (0.4.10)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (4.1.0)
......@@ -663,7 +663,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta43)
prometheus-client-mmap (0.7.0.beta44)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
......@@ -737,7 +737,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
redis (3.3.5)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
......@@ -868,11 +868,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
sidekiq (5.0.4)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
redis (>= 3.3.4, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
......@@ -1081,7 +1081,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.61.0)
gitaly-proto (~> 0.62.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1124,7 +1124,7 @@ DEPENDENCIES
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5)
mysql2 (~> 0.4.10)
net-ldap
net-ntp
net-ssh (~> 4.1.0)
......@@ -1160,7 +1160,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta43)
prometheus-client-mmap (~> 0.7.0.beta44)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
......@@ -111,7 +111,6 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
......
......@@ -140,7 +140,7 @@ class FilteredSearchManager {
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
......@@ -193,22 +193,34 @@ class FilteredSearchManager {
this.unbindStateEvents();
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
checkForBackspace() {
let backspaceCount = 0;
// closure for keeping track of the number of backspace keystrokes
return (e) => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
backspaceCount += 1;
if (backspaceCount === 2) {
backspaceCount = 0;
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
}
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
} else {
backspaceCount = 0;
}
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
};
}
checkForEnter(e) {
......
......@@ -4,8 +4,11 @@ import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
import MilestoneSelect from './milestone_select';
<<<<<<< HEAD
import WeightSelect from 'ee/weight_select'; // eslint-disable-line import/first
=======
>>>>>>> upstream/master
export default () => {
new UsersSelect();
......
import _ from 'underscore';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { timeFor } from './lib/utils/datetime_utility';
......@@ -193,7 +193,7 @@ export default class Job {
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
const size = bytesToKiB(this.logBytes);
const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {
......
......@@ -226,5 +226,8 @@ export default class MilestoneSelect {
});
}
}
<<<<<<< HEAD
window.MilestoneSelect = MilestoneSelect;
=======
>>>>>>> upstream/master
......@@ -10,7 +10,6 @@
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
font-size: 14px;
line-height: 36px;
&.diff-collapsed {
padding: 5px;
......
......@@ -358,10 +358,6 @@
}
.sidebar-top-level-items > li {
&.active a {
padding-left: 12px;
}
.sidebar-sub-level-items {
&:not(.flyout-list) {
display: none;
......
......@@ -12,6 +12,7 @@
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid $list-border;
word-wrap: break-word;
&::after {
content: " ";
......
......@@ -159,7 +159,6 @@
}
}
/*
* Last push widget
*/
......@@ -182,6 +181,12 @@
.event-item {
padding-left: 0;
&.event-inline {
.event-title {
line-height: 20px;
}
}
.event-title {
white-space: normal;
overflow: visible;
......
class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
skip_before_action :require_no_authentication, only: [:edit, :update]
before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
......
......@@ -46,14 +46,16 @@ class Projects::BranchesController < Projects::ApplicationController
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
if params[:issue_iid]
success = (result[:status] == :success)
if params[:issue_iid] && success
issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
respond_to do |format|
format.html do
if result[:status] == :success
if success
if redirect_to_autodeploy
redirect_to url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name)
......@@ -67,7 +69,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
format.json do
if result[:status] == :success
if success
render json: { name: branch_name, url: project_tree_url(@project, branch_name) }
else
render json: result[:messsage], status: :unprocessable_entity
......
......@@ -29,17 +29,17 @@ class Projects::RunnersController < Projects::ApplicationController
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end
......
......@@ -23,8 +23,13 @@ class DiffDiscussion < Discussion
def merge_request_version_params
return unless for_merge_request?
version_params = get_params
return version_params unless on_merge_request_commit? && commit_id
version_params ||= {}
version_params.tap do |params|
params[:commit_id] = commit_id if on_merge_request_commit?
params[:commit_id] = commit_id
end
end
......@@ -37,7 +42,7 @@ class DiffDiscussion < Discussion
private
def version_params
def get_params
return {} if active?
noteable.version_params_for(position.diff_refs)
......
......@@ -24,7 +24,11 @@ class ProjectTeam
end
def add_role(user, role, current_user: nil)
<<<<<<< HEAD
public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
=======
send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
>>>>>>> upstream/master
end
def find_member(user_id)
......
......@@ -1059,10 +1059,6 @@ class Repository
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
def remote_exists?(name)
raw_repository.remote_exists?(name)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end
......
......@@ -96,8 +96,8 @@ class User < ActiveRecord::Base
has_one :user_synced_attributes_metadata, autosave: true
# Groups
has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
......@@ -105,7 +105,7 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -23,7 +23,7 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
......
......@@ -26,7 +26,7 @@ module Projects
name: @project.name,
path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
namespace_id: target_namespace.id
}
if @project.avatar.present? && @project.avatar.image?
......@@ -74,14 +74,14 @@ module Projects
Projects::ForksCountService.new(@project).refresh_cache
end
def target_namespace
@target_namespace ||= @params[:namespace] || current_user.namespace
end
def allowed_visibility_level
project_level = @project.visibility_level
target_level = [@project.visibility_level, target_namespace.visibility_level].min
if Gitlab::VisibilityLevel.non_restricted_level?(project_level)
project_level
else
Gitlab::VisibilityLevel.highest_allowed_level
end
Gitlab::VisibilityLevel.closest_allowed_level(target_level)
end
end
end
......@@ -31,6 +31,11 @@ module Users
return user
end
# Calling all before/after_destroy hooks for the user because
# there is no dependent: destroy in the relationship. And the removal
# is done by a foreign_key. Otherwise they won't be called
user.members.find_each { |member| member.run_callbacks(:destroy) }
user.solo_owned_groups.each do |group|
Groups::DestroyService.new(group, current_user).execute
end
......
- add_to_breadcrumbs "Applications", oauth_applications_path
- breadcrumb_title @application.name
- page_title @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
......
......@@ -131,7 +131,7 @@
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_broadcast_messages_path do
= link_to admin_abuse_reports_path do
%strong.fly-out-top-item-name
#{ _('Abuse Reports') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
......
......@@ -4,7 +4,7 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h3.prepend-top-0
%h4.prepend-top-0
= page_title
%p
This is a security log of important events involving your account.
......
- page_title "GPG Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
......
- add_to_breadcrumbs "SSH Keys", profile_keys_path
- breadcrumb_title @key.title
- page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
......
......@@ -10,16 +10,16 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
.row
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4
%h4.prepend-top-0
= page_title
%p
You can specify notification level per group or per project.
%p
By default, all projects and groups will use the global notifications setting.
.col-lg-8
%h5
%h5.prepend-top-0
Global notification settings
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
......
......@@ -62,7 +62,7 @@
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
.controllers.pull-right
......
......@@ -4,14 +4,17 @@
= render 'projects/merge_requests/diffs/commit_widget'
- if @merge_request_diff&.empty?
.nothing-here-block
= image_tag 'illustrations/merge_request_changes_empty.svg'
= succeed '.' do
No changes between
%span.ref-name= @merge_request.source_branch
and
%span.ref-name= @merge_request.target_branch
%p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
.row.empty-state.nothing-here-block
.col-xs-12
.svg-content= image_tag 'illustrations/merge_request_changes_empty.svg'
.col-xs-12
.text-content.text-center
%p
No changes between
%span.ref-name= @merge_request.source_branch
and
%span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else
- diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
- if diff_viewable
......
......@@ -17,6 +17,10 @@
.pull-right
- if @project_runners.include?(runner)
- if runner.active?
= link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" }
- else
= link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
- if runner.belongs_to_one_project?
= link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
......
- resource_name = spammable.class.model_name.singular
- humanized_resource_name = spammable.class.model_name.human.downcase
- script = local_assigns.fetch(:script, true)
- method = params[:action] == 'create' ? :post : :put
- has_submit = local_assigns.fetch(:has_submit, true)
= form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
.recaptcha
- params[resource_name].each do |field, value|
= hidden_field(resource_name, field, value: value)
......
---
title: "Ignore lost+found folder during backup on a volume"
merge_request: 16036
author: Julien Millau
type: fixed
\ No newline at end of file
---
title: Improve search query for issues.
merge_request:
author:
type: performance
---
title: Add pause/resume button to project runners
merge_request: 16032
author: Mario de la Ossa
type: added
---
title: Fix when branch creation fails don't post system note
merge_request:
author: Mateusz Bajorski
type: fixed
---
title: Support new chat notifications parameters in Services API
merge_request: 11435
author:
type: added
---
title: Clears visual token on second backspace
merge_request:
author: Martin Wortschack
type: fixed
---
title: Fix breadcrumbs in User Settings
merge_request: 16172
author: rfwatson
type: fixed
---
title: Bump mysql2 gem version from 0.4.5 to 0.4.10
merge_request:
author: asaparov
type: other
---
title: Don't link LFS objects to a project when unlinking forks when they were already
linked
merge_request: 16006
author:
type: fixed
---
title: Allow forking a public project to a private group
merge_request: 16050
author:
type: changed
---
title: Fix abuse reports link url in admin area navbar
merge_request: 16068
author: megos
type: fixed
---
title: Fix activity inline event line height on mobile
merge_request: 16121
author: George Tsiolis
type: fixed
---
title: Adjust content width for User Settings, GPG Keys
merge_request: 16093
author: George Tsiolis
type: fixed
---
title: Keep typographic hierarchy in User Settings
merge_request: 16090
author: George Tsiolis
type: fixed
---
title: Remove unnecessary sidebar element realignment
merge_request: 16159
author: George Tsiolis
type: fixed
---
title: Fixing error 500 when member exist but not the user
merge_request: 15970
author:
type: fixed
---
title: Reduce the number of buckets in gitlab_cache_operation_duration_seconds metric
merge_request: 15881
author:
type: changed
---
title: Execute project hooks and services after commit when moving an issue
title: Gracefully handle garbled URIs in Markdown
merge_request:
author:
type: fixed
---
title: Humanize the units of "Showing last X KiB of log" in job trace
merge_request:
author:
type: fixed
---
title: show None when issue is in closed list and no labels assigned
merge_request: 15976
author: Christiaan Van den Poel
type: fixed
......@@ -17,6 +17,11 @@ end
require ::File.expand_path('../config/environment', __FILE__)
warmup do |app|
client = Rack::MockRequest.new(app)
client.get('/')
end
map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do
run Gitlab::Application
end
......@@ -79,3 +79,8 @@ elsif Gitlab::Database.mysql?
NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamp' }
end
end
# Ensure `datetime_with_timezone` columns are correctly written to schema.rb
if (ActiveRecord::Base.connection.active? rescue false)
ActiveRecord::Base.connection.send :reload_type_map
end
......@@ -14,8 +14,8 @@ AssetSync.configure do |config|
config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] if ENV.has_key?('AWS_ACCESS_KEY_ID')
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('AWS_SECRET_ACCESS_KEY')
config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID')
config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY')
config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
......
......@@ -423,8 +423,8 @@ constraints(ProjectUrlConstrainer.new) do
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
get :resume
get :pause
post :resume
post :pause
end
collection do
......
class FixDevTimezoneSchema < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# The this migrations tries to help solve unwanted changes to `schema.rb`
# while developing GitLab. Installations created before we started using
# `datetime_with_timezone` are likely to face this problem. Updating those
# columns to the new type should help fix this.
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
TIMEZONE_TABLES = %i(appearances ci_group_variables ci_pipeline_schedule_variables events gpg_keys gpg_signatures project_auto_devops)
def up
return unless Rails.env.development? || Rails.env.test?
TIMEZONE_TABLES.each do |table|
change_column table, :created_at, :datetime_with_timezone
change_column table, :updated_at, :datetime_with_timezone
end
end
def down
end
end
......@@ -15,8 +15,20 @@ class IssuesMovedToIdForeignKey < ActiveRecord::Migration
self.table_name = 'issues'
def self.with_orphaned_moved_to_issues
where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
.where('moved_to_id IS NOT NULL')
if Gitlab::Database.postgresql?
# Be careful to use a second table here for comparison otherwise we'll null
# out all rows that don't have id == moved.to_id!
where('NOT EXISTS (SELECT true FROM issues B WHERE issues.moved_to_id = B.id)')
.where('moved_to_id IS NOT NULL')
else
# MySQL doesn't allow modification of the same table in a subquery,
# and using a temporary table isn't automatically guaranteed to work
# due to the MySQL query optimizer. See
# https://dev.mysql.com/doc/refman/5.7/en/update.html for more
# details.
joins('LEFT JOIN issues AS b ON issues.moved_to_id = b.id')
.where('issues.moved_to_id IS NOT NULL AND b.id IS NULL')
end
end
end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CleanUpForMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class Member < ActiveRecord::Base
include EachBatch
self.table_name = 'members'
end
def up
condition = <<~EOF.squish
invite_token IS NULL AND
NOT EXISTS (SELECT 1 FROM users WHERE users.id = members.user_id)
EOF
Member.each_batch(of: 10_000) do |batch|
batch.where(condition).delete_all
end
end
def down
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddForeignKeyForMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key(:members,
:users,
column: :user_id)
end
def down
remove_foreign_key(:members, column: :user_id)
end
end
......@@ -749,8 +749,8 @@ ActiveRecord::Schema.define(version: 20171220191323) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.datetime_with_timezone "confirmed_at"
t.datetime_with_timezone "confirmation_sent_at"
end
add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
......@@ -2254,8 +2254,8 @@ ActiveRecord::Schema.define(version: 20171220191323) 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_custom_attributes", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id", null: false
t.string "key", null: false
t.string "value", null: false
......@@ -2522,6 +2522,7 @@ ActiveRecord::Schema.define(version: 20171220191323) do
add_foreign_key "labels", "projects", name: "fk_7de4989a69", on_delete: :cascade
add_foreign_key "lists", "boards", name: "fk_0d3f677137", on_delete: :cascade
add_foreign_key "lists", "labels", name: "fk_7a5553d60f", on_delete: :cascade
add_foreign_key "members", "users", name: "fk_2e88fb7ce9", on_delete: :cascade
add_foreign_key "merge_request_diff_commits", "merge_request_diffs", on_delete: :cascade
add_foreign_key "merge_request_diff_files", "merge_request_diffs", on_delete: :cascade
add_foreign_key "merge_request_diffs", "merge_requests", name: "fk_8483f3258f", on_delete: :cascade
......
......@@ -58,30 +58,32 @@ our AsciiDoc snippets, wikis and repos using delimited blocks:
- **Markdown**
```plantuml
Bob -> Alice : hello
Alice -> Bob : Go Away
```
<pre>
```plantuml
Bob -> Alice : hello
Alice -> Bob : Go Away
```
</pre>
- **AsciiDoc**
```
<pre>
[plantuml, format="png", id="myDiagram", width="200px"]
--
Bob->Alice : hello
Alice -> Bob : Go Away
--
```
</pre>
- **reStructuredText**
```
<pre>
.. plantuml::
:caption: Caption with **bold** and *italic*
Bob -> Alice: hello
Alice -> Bob: Go Away
```
</pre>
You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option.
......
This diff is collapsed.
......@@ -16,8 +16,7 @@ codequality:
- docker:dind
script:
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate init
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json || true
artifacts:
paths: [codeclimate.json]
```
......
......@@ -27,6 +27,7 @@ comments: false
## Backend guides
- [GitLab utilities](utilities.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
......
# GitLab utilities
We developed a number of utilities to ease development.
## [`MergeHash`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/merge_hash.rb)
* Deep merges an array of hashes:
``` ruby
Gitlab::Utils::MergeHash.merge(
[{ hello: ["world"] },
{ hello: "Everyone" },
{ hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] } },
"Goodbye", "Hallo"]
)
```
Gives:
``` ruby
[
{
hello:
[
"world",
"Everyone",
{ greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] }
]
},
"Goodbye"
]
```
* Extracts all keys and values from a hash into an array:
``` ruby
Gitlab::Utils::MergeHash.crush(
{ hello: "world", this: { crushes: ["an entire", "hash"] } }
)
```
Gives:
``` ruby
[:hello, "world", :this, :crushes, "an entire", "hash"]
```
## [`StrongMemoize`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/strong_memoize.rb)
* Memoize the value even if it is `nil` or `false`.
We often do `@value ||= compute`, however this doesn't work well if
`compute` might eventually give `nil` and we don't want to compute again.
Instead we could use `defined?` to check if the value is set or not.
However it's tedious to write such pattern, and `StrongMemoize` would
help us use such pattern.
Instead of writing patterns like this:
``` ruby
class Find
def result
return @result if defined?(@result)
@result = search
end
end
```
We could write it like:
``` ruby
class Find
include Gitlab::Utils::StrongMemoize
def result
strong_memoize(:result) do
search
end
end
end
```
* Clear memoization
``` ruby
class Find
include Gitlab::Utils::StrongMemoize
end
Find.new.clear_memoization(:result)
```
......@@ -170,6 +170,7 @@ where `PROJECT-1` is the issue ID of the JIRA project.
- Only commits and merges into the project's default branch (usually **master**) will
close an issue in Jira. You can change your projects default branch under
[project settings](img/jira_project_settings.png).
- The JIRA issue will not be transitioned if it has a resolution.
### JIRA issue closing example
......@@ -219,6 +220,10 @@ JIRA issue references and update comments will not work if the GitLab issue trac
Make sure the `Transition ID` you set within the JIRA settings matches the one
your project needs to close a ticket.
Make sure that the JIRA issue is not already marked as resolved, in other words that
the JIRA issue resolution field is not set. (It should not be struck through in
JIRA lists.)
[services-templates]: services_templates.md
[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
[jira]: https://www.atlassian.com/software/jira
......@@ -41,9 +41,15 @@ do.
| `/clear_weight` | Clears the issue weight |
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
<<<<<<< HEAD
| `/move path/to/project` | Moves issue to another project |
| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
Note: In GitLab EES every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign`
support multiple assignees.
=======
| `/move path/to/project` | Moves issue to another project |
| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
>>>>>>> upstream/master
......@@ -21,7 +21,11 @@ module SharedProject
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
<<<<<<< HEAD
@project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace, issues_template: "This issue should contain the following.", merge_requests_template: "This merge request should contain the following.")
=======
@project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
>>>>>>> upstream/master
@project.add_master(@user)
end
......
......@@ -29,6 +29,9 @@ Capybara.register_driver :chrome do |app|
options.add_argument("disable-gpu")
end
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
......
......@@ -786,8 +786,9 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
expose :tag_push_events, :note_events, :pipeline_events
expose :push_events, :issues_events, :confidential_issues_events
expose :merge_requests_events, :tag_push_events, :note_events
expose :pipeline_events, :wiki_page_events
expose :job_events
# Expose serialized properties
expose :properties do |service, options|
......
......@@ -202,9 +202,12 @@ module API
project = Gitlab::GlRepository.parse(params[:gl_repository]).first
user = identify(params[:identifier])
redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
if redirect_message
output[:redirected_message] = redirect_message
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
if user
redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
output[:redirected_message] = redirect_message if redirect_message
end
output
......
......@@ -65,7 +65,9 @@ module API
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
if member.persisted? && member.valid?
if !member
not_allowed! # This currently can only be reached in EE
elsif member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
render_validation_error!(member)
......
module API
class Services < Grape::API
chat_notification_settings = [
{
required: true,
name: :webhook,
type: String,
desc: 'The chat webhook'
},
{
required: false,
name: :username,
type: String,
desc: 'The chat username'
},
{
required: false,
name: :channel,
type: String,
desc: 'The default chat channel'
}
]
chat_notification_flags = [
{
required: false,
name: :notify_only_broken_pipelines,
type: Boolean,
desc: 'Send notifications for broken pipelines'
},
{
required: false,
name: :notify_only_default_branch,
type: Boolean,
desc: 'Send notifications only for the default branch'
}
]
chat_notification_channels = [
{
required: false,
name: :push_channel,
type: String,
desc: 'The name of the channel to receive push_events notifications'
},
{
required: false,
name: :issue_channel,
type: String,
desc: 'The name of the channel to receive issues_events notifications'
},
{
required: false,
name: :confidential_issue_channel,
type: String,
desc: 'The name of the channel to receive confidential_issues_events notifications'
},
{
required: false,
name: :merge_request_channel,
type: String,
desc: 'The name of the channel to receive merge_requests_events notifications'
},
{
required: false,
name: :note_channel,
type: String,
desc: 'The name of the channel to receive note_events notifications'
},
{
required: false,
name: :tag_push_channel,
type: String,
desc: 'The name of the channel to receive tag_push_events notifications'
},
{
required: false,
name: :pipeline_channel,
type: String,
desc: 'The name of the channel to receive pipeline_events notifications'
},
{
required: false,
name: :wiki_page_channel,
type: String,
desc: 'The name of the channel to receive wiki_page_events notifications'
}
]
chat_notification_events = [
{
required: false,
name: :push_events,
type: Boolean,
desc: 'Enable notifications for push_events'
},
{
required: false,
name: :issues_events,
type: Boolean,
desc: 'Enable notifications for issues_events'
},
{
required: false,
name: :confidential_issues_events,
type: Boolean,
desc: 'Enable notifications for confidential_issues_events'
},
{
required: false,
name: :merge_requests_events,
type: Boolean,
desc: 'Enable notifications for merge_requests_events'
},
{
required: false,
name: :note_events,
type: Boolean,
desc: 'Enable notifications for note_events'
},
{
required: false,
name: :tag_push_events,
type: Boolean,
desc: 'Enable notifications for tag_push_events'
},
{
required: false,
name: :pipeline_events,
type: Boolean,
desc: 'Enable notifications for pipeline_events'
},
{
required: false,
name: :wiki_page_events,
type: Boolean,
desc: 'Enable notifications for wiki_page_events'
}
]
services = {
'asana' => [
{
......@@ -488,25 +626,11 @@ module API
}
],
'slack' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
},
{
required: false,
name: :new_issue_url,
type: String,
desc: 'The user name'
},
{
required: false,
name: :channel,
type: String,
desc: 'The channel name'
}
],
chat_notification_settings,
chat_notification_flags,
chat_notification_channels,
chat_notification_events
].flatten,
'microsoft-teams' => [
{
required: true,
......@@ -516,19 +640,11 @@ module API
}
],
'mattermost' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
},
{
required: false,
name: :username,
type: String,
desc: 'The username to use to post the message'
}
],
chat_notification_settings,
chat_notification_flags,
chat_notification_channels,
chat_notification_events
].flatten,
'teamcity' => [
{
required: true,
......
......@@ -18,7 +18,7 @@ module Backup
FileUtils.rm_f(backup_tarball)
if ENV['STRATEGY'] == 'copy'
cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path})
cmd = %W(rsync -a --exclude=lost+found #{app_files_dir} #{Gitlab.config.backup.path})
output, status = Gitlab::Popen.popen(cmd)
unless status.zero?
......@@ -26,10 +26,10 @@ module Backup
abort 'Backup failed'
end
run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
end
end
......
......@@ -66,7 +66,7 @@ module Banzai
if uri.relative? && uri.path.present?
html_attr.value = rebuild_relative_uri(uri).to_s
end
rescue URI::Error
rescue URI::Error, Addressable::URI::InvalidURIError
# noop
end
......
......@@ -21,6 +21,10 @@ module Gitlab
end
def add_redirect_message
# Don't bother with sending a redirect message for anonymous clones
# because they never see it via the `/internal/post_receive` endpoint
return unless user.present? && project.present?
Gitlab::Redis::SharedState.with do |redis|
key = self.class.redirect_message_key(user.id, project.id)
redis.setex(key, 5.minutes, redirect_message)
......
......@@ -13,12 +13,13 @@ module Gitlab
end
def resolve(user, commit_message, files)
msg = commit_message || default_commit_message
resolution = Gitlab::Git::Conflict::Resolution.new(user, files, msg)
args = {
source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch,
commit_message: commit_message || default_commit_message
target_branch: merge_request.target_branch
}
resolver.resolve_conflicts(@source_repo, user, files, args)
resolver.resolve_conflicts(@source_repo, resolution, args)
ensure
@merge_request.clear_memoized_shas
end
......
......@@ -71,6 +71,16 @@ module Gitlab
end
end
def encode_binary(s)
return "" if s.nil?
s.dup.force_encoding(Encoding::ASCII_8BIT)
end
def binary_stringio(s)
StringIO.new(s || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
end
private
def clean(message)
......
......@@ -2,7 +2,9 @@ module Gitlab
module Git
module Conflict
class File
attr_reader :content, :their_path, :our_path, :our_mode, :repository, :commit_oid
attr_reader :their_path, :our_path, :our_mode, :repository, :commit_oid
attr_accessor :content
def initialize(repository, commit_oid, conflict, content)
@repository = repository
......
module Gitlab
module Git
module Conflict
class Resolution
attr_reader :user, :files, :commit_message
def initialize(user, files, commit_message)
@user = user
@files = files
@commit_message = commit_message
end
end
end
end
end
......@@ -13,37 +13,27 @@ module Gitlab
def conflicts
@conflicts ||= begin
target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
conflict_files(@target_repository, target_index)
@target_repository.gitaly_migrate(:conflicts_list_conflict_files) do |is_enabled|
if is_enabled
gitaly_conflicts_client(@target_repository).list_conflict_files
else
rugged_list_conflict_files
end
end
end
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
rescue Rugged::OdbError, GRPC::BadStatus => e
raise Gitlab::Git::CommandError.new(e)
end
def resolve_conflicts(source_repository, user, files, source_branch:, target_branch:, commit_message:)
source_repository.with_repo_branch_commit(@target_repository, target_branch) do
index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
conflicts = conflict_files(source_repository, index)
files.each do |file_params|
conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(source_repository, index, conflict_file, file_params)
end
unless index.conflicts.empty?
missing_files = index.conflicts.map { |file| file[:ours][:path] }
raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
source_repository.gitaly_migrate(:conflicts_resolve_conflicts) do |is_enabled|
if is_enabled
gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
else
rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
end
commit_params = {
message: commit_message,
parents: [@our_commit_oid, @their_commit_oid]
}
source_repository.commit_index(user, source_branch, index, commit_params)
end
end
......@@ -68,6 +58,10 @@ module Gitlab
end
end
def gitaly_conflicts_client(repository)
repository.gitaly_conflicts_client(@our_commit_oid, @their_commit_oid)
end
def write_resolved_file_to_index(repository, index, file, params)
if params[:sections]
resolved_lines = file.resolve_lines(params[:sections])
......@@ -84,6 +78,40 @@ module Gitlab
index.add(path: our_path, oid: oid, mode: file.our_mode)
index.conflict_remove(our_path)
end
def rugged_list_conflict_files
target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
conflict_files(@target_repository, target_index)
end
def rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
source_repository.with_repo_branch_commit(@target_repository, target_branch) do
index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
conflicts = conflict_files(source_repository, index)
resolution.files.each do |file_params|
conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(source_repository, index, conflict_file, file_params)
end
unless index.conflicts.empty?
missing_files = index.conflicts.map { |file| file[:ours][:path] }
raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
message: resolution.commit_message,
parents: [@our_commit_oid, @their_commit_oid]
}
source_repository.commit_index(resolution.user, source_branch, index, commit_params)
end
end
end
end
end
......
......@@ -126,7 +126,7 @@ module Gitlab
end
def exists?
Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
if enabled
gitaly_repository_client.exists?
else
......@@ -188,7 +188,7 @@ module Gitlab
end
def local_branches(sort_by: nil)
gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
gitaly_migrate(:local_branches) do |is_enabled|
if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by)
else
......@@ -919,31 +919,23 @@ module Gitlab
# If `mirror_refmap` is present the remote is set as mirror with that mapping
def add_remote(remote_name, url, mirror_refmap: nil)
rugged.remotes.create(remote_name, url)
set_remote_as_mirror(remote_name, refmap: mirror_refmap) if mirror_refmap
rescue Rugged::ConfigError
remote_update(remote_name, url: url)
gitaly_migrate(:remote_add_remote) do |is_enabled|
if is_enabled
gitaly_remote_client.add_remote(remote_name, url, mirror_refmap)
else
rugged_add_remote(remote_name, url, mirror_refmap)
end
end
end
def remove_remote(remote_name)
# When a remote is deleted all its remote refs are deleted too, but in
# the case of mirrors we map its refs (that would usualy go under
# [remote_name]/) to the top level namespace. We clean the mapping so
# those don't get deleted.
if rugged.config["remote.#{remote_name}.mirror"]
rugged.config.delete("remote.#{remote_name}.fetch")
gitaly_migrate(:remote_remove_remote) do |is_enabled|
if is_enabled
gitaly_remote_client.remove_remote(remote_name)
else
rugged_remove_remote(remote_name)
end
end
rugged.remotes.delete(remote_name)
true
rescue Rugged::ConfigError
false
end
# Returns true if a remote exists.
def remote_exists?(name)
rugged.remotes[name].present?
end
# Update the specified remote using the values in the +options+ hash
......@@ -1298,6 +1290,14 @@ module Gitlab
@gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
def gitaly_remote_client
@gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
end
def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
end
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
......@@ -1917,6 +1917,29 @@ module Gitlab
raise ArgumentError, 'Invalid merge source'
end
def rugged_add_remote(remote_name, url, mirror_refmap)
rugged.remotes.create(remote_name, url)
set_remote_as_mirror(remote_name, refmap: mirror_refmap) if mirror_refmap
rescue Rugged::ConfigError
remote_update(remote_name, url: url)
end
def rugged_remove_remote(remote_name)
# When a remote is deleted all its remote refs are deleted too, but in
# the case of mirrors we map its refs (that would usualy go under
# [remote_name]/) to the top level namespace. We clean the mapping so
# those don't get deleted.
if rugged.config["remote.#{remote_name}.mirror"]
rugged.config.delete("remote.#{remote_name}.fetch")
end
rugged.remotes.delete(remote_name)
true
rescue Rugged::ConfigError
false
end
def fetch_remote(remote_name = 'origin', env: nil)
run_git(['fetch', remote_name], env: env).last.zero?
end
......
......@@ -330,22 +330,6 @@ module Gitlab
Google::Protobuf::Timestamp.new(seconds: t.to_i)
end
def self.encode(s)
return "" if s.nil?
s.dup.force_encoding(Encoding::ASCII_8BIT)
end
def self.binary_stringio(s)
io = StringIO.new(s || '')
io.set_encoding(Encoding::ASCII_8BIT)
io
end
def self.encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
end
# The default timeout on all Gitaly calls
def self.default_timeout
return 0 if Sidekiq.server?
......
module Gitlab
module GitalyClient
class CommitService
include Gitlab::EncodingHelper
# The ID of empty tree.
# See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
......@@ -13,7 +15,7 @@ module Gitlab
def ls_files(revision)
request = Gitaly::ListFilesRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout)
......@@ -73,7 +75,7 @@ module Gitlab
request = Gitaly::TreeEntryRequest.new(
repository: @gitaly_repo,
revision: ref,
path: GitalyClient.encode(path),
path: encode_binary(path),
limit: limit.to_i
)
......@@ -98,8 +100,8 @@ module Gitlab
def tree_entries(repository, revision, path)
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision),
path: path.present? ? GitalyClient.encode(path) : '.'
revision: encode_binary(revision),
path: path.present? ? encode_binary(path) : '.'
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
......@@ -112,8 +114,8 @@ module Gitlab
type: gitaly_tree_entry.type.downcase,
mode: gitaly_tree_entry.mode.to_s(8),
name: File.basename(gitaly_tree_entry.path),
path: GitalyClient.encode(gitaly_tree_entry.path),
flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path),
path: encode_binary(gitaly_tree_entry.path),
flat_path: encode_binary(gitaly_tree_entry.flat_path),
commit_id: gitaly_tree_entry.commit_oid
)
end
......@@ -135,8 +137,8 @@ module Gitlab
def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision),
path: GitalyClient.encode(path.to_s)
revision: encode_binary(revision),
path: encode_binary(path.to_s)
)
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit
......@@ -202,8 +204,8 @@ module Gitlab
def raw_blame(revision, path)
request = Gitaly::RawBlameRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision),
path: GitalyClient.encode(path)
revision: encode_binary(revision),
path: encode_binary(path)
)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
......@@ -213,7 +215,7 @@ module Gitlab
def find_commit(revision)
request = Gitaly::FindCommitRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
......@@ -224,7 +226,7 @@ module Gitlab
def patch(revision)
request = Gitaly::CommitPatchRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
......@@ -234,7 +236,7 @@ module Gitlab
def commit_stats(revision)
request = Gitaly::CommitStatsRequest.new(
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
revision: encode_binary(revision)
)
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout)
end
......@@ -250,9 +252,9 @@ module Gitlab
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = GitalyClient.encode(options[:ref]) if options[:ref]
request.revision = encode_binary(options[:ref]) if options[:ref]
request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
......@@ -264,7 +266,7 @@ module Gitlab
enum = Enumerator.new do |y|
shas.each_slice(20) do |revs|
request.shas = GitalyClient.encode_repeated(revs)
request.shas = encode_repeated(revs)
y.yield request
......@@ -303,7 +305,7 @@ module Gitlab
repository: @gitaly_repo,
left_commit_id: from_id,
right_commit_id: to_id,
paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) }
paths: options.fetch(:paths, []).compact.map { |path| encode_binary(path) }
}
end
......@@ -314,6 +316,10 @@ module Gitlab
end
end
end
def encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
end
end
end
end
module Gitlab
module GitalyClient
class ConflictsService
MAX_MSG_SIZE = 128.kilobytes.freeze
def initialize(repository, our_commit_oid, their_commit_oid)
@gitaly_repo = repository.gitaly_repository
@repository = repository
@our_commit_oid = our_commit_oid
@their_commit_oid = their_commit_oid
end
def list_conflict_files
request = Gitaly::ListConflictFilesRequest.new(
repository: @gitaly_repo,
our_commit_oid: @our_commit_oid,
their_commit_oid: @their_commit_oid
)
response = GitalyClient.call(@repository.storage, :conflicts_service, :list_conflict_files, request)
files_from_response(response).to_a
end
def resolve_conflicts(target_repository, resolution, source_branch, target_branch)
reader = GitalyClient.binary_stringio(resolution.files.to_json)
req_enum = Enumerator.new do |y|
header = resolve_conflicts_request_header(target_repository, resolution, source_branch, target_branch)
y.yield Gitaly::ResolveConflictsRequest.new(header: header)
until reader.eof?
chunk = reader.read(MAX_MSG_SIZE)
y.yield Gitaly::ResolveConflictsRequest.new(files_json: chunk)
end
end
response = GitalyClient.call(@repository.storage, :conflicts_service, :resolve_conflicts, req_enum, remote_storage: target_repository.storage)
if response.resolution_error.present?
raise Gitlab::Git::Conflict::Resolver::ResolutionError, response.resolution_error
end
end
private
def resolve_conflicts_request_header(target_repository, resolution, source_branch, target_branch)
Gitaly::ResolveConflictsRequestHeader.new(
repository: @gitaly_repo,
our_commit_oid: @our_commit_oid,
target_repository: target_repository.gitaly_repository,
their_commit_oid: @their_commit_oid,
source_branch: source_branch,
target_branch: target_branch,
commit_message: resolution.commit_message,
user: Gitlab::Git::User.from_gitlab(resolution.user).to_gitaly
)
end
def files_from_response(response)
files = []
response.each do |msg|
msg.files.each do |gitaly_file|
if gitaly_file.header
files << file_from_gitaly_header(gitaly_file.header)
else
files.last.content << gitaly_file.content
end
end
end
files
end
def file_from_gitaly_header(header)
Gitlab::Git::Conflict::File.new(
Gitlab::GitalyClient::Util.git_repository(header.repository),
header.commit_oid,
conflict_from_gitaly_file_header(header),
''
)
end
def conflict_from_gitaly_file_header(header)
{
ours: { path: header.our_path, mode: header.our_mode },
theirs: { path: header.their_path }
}
end
end
end
end
module Gitlab
module GitalyClient
class OperationService
include Gitlab::EncodingHelper
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
......@@ -9,7 +11,7 @@ module Gitlab
def rm_tag(tag_name, user)
request = Gitaly::UserDeleteTagRequest.new(
repository: @gitaly_repo,
tag_name: GitalyClient.encode(tag_name),
tag_name: encode_binary(tag_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
......@@ -24,9 +26,9 @@ module Gitlab
request = Gitaly::UserCreateTagRequest.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
tag_name: GitalyClient.encode(tag_name),
target_revision: GitalyClient.encode(target),
message: GitalyClient.encode(message.to_s)
tag_name: encode_binary(tag_name),
target_revision: encode_binary(target),
message: encode_binary(message.to_s)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request)
......@@ -44,9 +46,9 @@ module Gitlab
def user_create_branch(branch_name, user, start_point)
request = Gitaly::UserCreateBranchRequest.new(
repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name),
branch_name: encode_binary(branch_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
start_point: GitalyClient.encode(start_point)
start_point: encode_binary(start_point)
)
response = GitalyClient.call(@repository.storage, :operation_service,
:user_create_branch, request)
......@@ -64,7 +66,7 @@ module Gitlab
def user_delete_branch(branch_name, user)
request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name),
branch_name: encode_binary(branch_name),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
......@@ -89,8 +91,8 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
branch: GitalyClient.encode(target_branch),
message: GitalyClient.encode(message)
branch: encode_binary(target_branch),
message: encode_binary(message)
)
)
......@@ -111,7 +113,7 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
branch: GitalyClient.encode(target_branch)
branch: encode_binary(target_branch)
)
branch_update = GitalyClient.call(
......@@ -152,9 +154,9 @@ module Gitlab
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit,
branch_name: GitalyClient.encode(branch_name),
message: GitalyClient.encode(message),
start_branch_name: GitalyClient.encode(start_branch_name.to_s),
branch_name: encode_binary(branch_name),
message: encode_binary(message),
start_branch_name: encode_binary(start_branch_name.to_s),
start_repository: start_repository.gitaly_repository
)
......
......@@ -72,7 +72,7 @@ module Gitlab
end
def ref_exists?(ref_name)
request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: GitalyClient.encode(ref_name))
request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: encode_binary(ref_name))
response = GitalyClient.call(@storage, :ref_service, :ref_exists, request)
response.value
rescue GRPC::InvalidArgument => e
......@@ -82,7 +82,7 @@ module Gitlab
def find_branch(branch_name)
request = Gitaly::FindBranchRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(branch_name)
name: encode_binary(branch_name)
)
response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
......@@ -96,8 +96,8 @@ module Gitlab
def create_branch(ref, start_point)
request = Gitaly::CreateBranchRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(ref),
start_point: GitalyClient.encode(start_point)
name: encode_binary(ref),
start_point: encode_binary(start_point)
)
response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request)
......@@ -121,7 +121,7 @@ module Gitlab
def delete_branch(branch_name)
request = Gitaly::DeleteBranchRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(branch_name)
name: encode_binary(branch_name)
)
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
......
module Gitlab
module GitalyClient
class RemoteService
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
@storage = repository.storage
end
def add_remote(name, url, mirror_refmap)
request = Gitaly::AddRemoteRequest.new(
repository: @gitaly_repo, name: name, url: url,
mirror_refmap: mirror_refmap.to_s
)
GitalyClient.call(@storage, :remote_service, :add_remote, request)
end
def remove_remote(name)
request = Gitaly::RemoveRemoteRequest.new(repository: @gitaly_repo, name: name)
response = GitalyClient.call(@storage, :remote_service, :remove_remote, request)
response.result
end
end
end
end
module Gitlab
module GitalyClient
class RepositoryService
include Gitlab::EncodingHelper
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
......@@ -72,7 +74,7 @@ module Gitlab
def find_merge_base(*revisions)
request = Gitaly::FindMergeBaseRequest.new(
repository: @gitaly_repo,
revisions: revisions.map { |r| GitalyClient.encode(r) }
revisions: revisions.map { |r| encode_binary(r) }
)
response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request)
......
......@@ -18,6 +18,12 @@ module Gitlab
)
end
def git_repository(gitaly_repository)
Gitlab::Git::Repository.new(gitaly_repository.storage_name,
gitaly_repository.relative_path,
gitaly_repository.gl_repository)
end
def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
if gitaly_tag.target_commit.present?
commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
......
......@@ -3,6 +3,8 @@ require 'stringio'
module Gitlab
module GitalyClient
class WikiService
include Gitlab::EncodingHelper
MAX_MSG_SIZE = 128.kilobytes.freeze
def initialize(repository)
......@@ -13,12 +15,12 @@ module Gitlab
def write_page(name, format, content, commit_details)
request = Gitaly::WikiWritePageRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(name),
name: encode_binary(name),
format: format.to_s,
commit_details: gitaly_commit_details(commit_details)
)
strio = GitalyClient.binary_stringio(content)
strio = binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
......@@ -39,13 +41,13 @@ module Gitlab
def update_page(page_path, title, format, content, commit_details)
request = Gitaly::WikiUpdatePageRequest.new(
repository: @gitaly_repo,
page_path: GitalyClient.encode(page_path),
title: GitalyClient.encode(title),
page_path: encode_binary(page_path),
title: encode_binary(title),
format: format.to_s,
commit_details: gitaly_commit_details(commit_details)
)
strio = GitalyClient.binary_stringio(content)
strio = binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
......@@ -63,7 +65,7 @@ module Gitlab
def delete_page(page_path, commit_details)
request = Gitaly::WikiDeletePageRequest.new(
repository: @gitaly_repo,
page_path: GitalyClient.encode(page_path),
page_path: encode_binary(page_path),
commit_details: gitaly_commit_details(commit_details)
)
......@@ -73,9 +75,9 @@ module Gitlab
def find_page(title:, version: nil, dir: nil)
request = Gitaly::WikiFindPageRequest.new(
repository: @gitaly_repo,
title: GitalyClient.encode(title),
revision: GitalyClient.encode(version),
directory: GitalyClient.encode(dir)
title: encode_binary(title),
revision: encode_binary(version),
directory: encode_binary(dir)
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
......@@ -102,8 +104,8 @@ module Gitlab
def find_file(name, revision)
request = Gitaly::WikiFindFileRequest.new(
repository: @gitaly_repo,
name: GitalyClient.encode(name),
revision: GitalyClient.encode(revision)
name: encode_binary(name),
revision: encode_binary(revision)
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
......@@ -158,9 +160,9 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
name: GitalyClient.encode(commit_details.name),
email: GitalyClient.encode(commit_details.email),
message: GitalyClient.encode(commit_details.message)
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
)
end
end
......
......@@ -34,7 +34,7 @@ module Gitlab
private
def pod_resource(command)
Pod.new(command, @namespace.name, @kubeclient).generate
Gitlab::Kubernetes::Helm::Pod.new(command, @namespace.name, @kubeclient).generate
end
end
end
......
......@@ -5,7 +5,7 @@ module Gitlab
# Class for tracking timing information about method calls
class MethodCall
@@measurement_enabled_cache = Concurrent::AtomicBoolean.new(false)
@@measurement_enabled_cache_expires_at = Concurrent::AtomicFixnum.new(Time.now.to_i)
@@measurement_enabled_cache_expires_at = Concurrent::AtomicReference.new(Time.now.to_i)
MUTEX = Mutex.new
BASE_LABELS = { module: nil, method: nil }.freeze
attr_reader :real_time, :cpu_time, :call_count, :labels
......
......@@ -82,7 +82,10 @@ module Gitlab
end
def issues
issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
issues = IssuesFinder.new(current_user).execute
unless default_project_filter
issues = issues.where(project_id: project_ids_relation)
end
issues =
if query =~ /#(\d+)\z/
......
......@@ -57,11 +57,17 @@ module Gitlab
}
end
def highest_allowed_level
def allowed_levels
restricted_levels = current_application_settings.restricted_visibility_levels
allowed_levels = self.values - restricted_levels
allowed_levels.max || PRIVATE
self.values - restricted_levels
end
def closest_allowed_level(target_level)
highest_allowed_level = allowed_levels.select { |level| level <= target_level }.max
# If all levels are restricted, fall back to PRIVATE
highest_allowed_level || PRIVATE
end
def allowed_for?(user, level)
......
......@@ -2,10 +2,13 @@ module QA
module Page
module Menu
class Admin < Page::Base
<<<<<<< HEAD:qa/qa/page/menu/admin.rb
def go_to_geo_nodes
click_link 'Geo Nodes'
end
=======
>>>>>>> upstream/master:qa/qa/page/menu/admin.rb
def go_to_license
click_link 'License'
end
......
......@@ -8,11 +8,13 @@ invalid_changelogs = Dir['changelogs/**/*'].reject do |changelog|
begin
YAML.load_file(changelog)
rescue
rescue => exception
puts exception
end
end
if invalid_changelogs.any?
puts
puts "Invalid changelogs found!\n"
puts invalid_changelogs.sort
exit 1
......
......@@ -3,12 +3,10 @@
require ::File.expand_path('../lib/gitlab/popen', __dir__)
tasks = [
%w[bundle exec bundle-audit check --update],
%w[bundle exec rake config_lint],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
%w[bundle exec rake scss_lint],
%w[bundle exec rake brakeman],
%w[bundle exec license_finder],
%w[yarn run eslint],
%w[bundle exec rubocop --parallel],
......
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.
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.
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