Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
21e08b6197
commit
1bdf79827c
|
@ -109,3 +109,4 @@ include:
|
|||
- local: .gitlab/ci/releases.gitlab-ci.yml
|
||||
- local: .gitlab/ci/notify.gitlab-ci.yml
|
||||
- local: .gitlab/ci/dast.gitlab-ci.yml
|
||||
- local: .gitlab/ci/workhorse.gitlab-ci.yml
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
workhorse:
|
||||
image: golang:1.14
|
||||
stage: test
|
||||
needs: []
|
||||
script:
|
||||
- rm .git/hooks/post-checkout
|
||||
- git checkout .
|
||||
- scripts/update-workhorse check
|
||||
- make -C workhorse
|
|
@ -32,6 +32,7 @@ AllCops:
|
|||
- 'builds/**/*'
|
||||
- 'plugins/**/*'
|
||||
- 'file_hooks/**/*'
|
||||
- 'workhorse/**/*'
|
||||
CacheRootDirectory: tmp
|
||||
MaxFilesInCache: 18000
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
a5a5d83630f13c3eb3e1650a24423fc5e9bc47d2
|
||||
3cbd24e3e2fd09eb526d04f8a419f6d103c440dc
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -115,7 +115,7 @@ gem 'carrierwave', '~> 1.3'
|
|||
gem 'mini_magick', '~> 4.10.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 3.6'
|
||||
gem 'fog-aws', '~> 3.7'
|
||||
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
|
||||
# Also see config/initializers/fog_core_patch.rb.
|
||||
gem 'fog-core', '= 2.1.0'
|
||||
|
@ -312,7 +312,7 @@ gem 'gitlab-pg_query', '~> 1.3', require: 'pg_query'
|
|||
gem 'premailer-rails', '~> 1.10.3'
|
||||
|
||||
# LabKit: Tracing and Correlation
|
||||
gem 'gitlab-labkit', '0.13.1'
|
||||
gem 'gitlab-labkit', '0.13.3'
|
||||
|
||||
# I18n
|
||||
gem 'ruby_parser', '~> 3.15', require: false
|
||||
|
|
|
@ -363,7 +363,7 @@ GEM
|
|||
fog-json
|
||||
ipaddress (~> 0.8)
|
||||
xml-simple (~> 1.1)
|
||||
fog-aws (3.6.7)
|
||||
fog-aws (3.7.0)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (~> 1.1)
|
||||
fog-xml (~> 0.1)
|
||||
|
@ -434,9 +434,10 @@ GEM
|
|||
fog-json (~> 1.2.0)
|
||||
mime-types
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
gitlab-labkit (0.13.1)
|
||||
gitlab-labkit (0.13.3)
|
||||
actionpack (>= 5.0.0, < 6.1.0)
|
||||
activesupport (>= 5.0.0, < 6.1.0)
|
||||
gitlab-pg_query (~> 1.3)
|
||||
grpc (~> 1.19)
|
||||
jaeger-client (~> 1.1)
|
||||
opentracing (~> 0.4)
|
||||
|
@ -1335,7 +1336,7 @@ DEPENDENCIES
|
|||
flipper-active_support_cache_store (~> 0.17.1)
|
||||
flowdock (~> 0.7)
|
||||
fog-aliyun (~> 0.3)
|
||||
fog-aws (~> 3.6)
|
||||
fog-aws (~> 3.7)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.11)
|
||||
fog-local (~> 0.6)
|
||||
|
@ -1352,7 +1353,7 @@ DEPENDENCIES
|
|||
github-markup (~> 1.7.0)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-fog-azure-rm (~> 1.0)
|
||||
gitlab-labkit (= 0.13.1)
|
||||
gitlab-labkit (= 0.13.3)
|
||||
gitlab-license (~> 1.0)
|
||||
gitlab-mail_room (~> 0.0.7)
|
||||
gitlab-markup (~> 1.7.1)
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class ImageFile {
|
|||
|
||||
initViewModes() {
|
||||
const viewMode = viewModes[0];
|
||||
$('.view-modes', this.file).removeClass('hide');
|
||||
$('.view-modes', this.file).removeClass('gl-display-none');
|
||||
$('.view-modes-menu', this.file).on('click', 'li', event => {
|
||||
if (!$(event.currentTarget).hasClass('active')) {
|
||||
return this.activateViewMode(event.currentTarget.className);
|
||||
|
@ -42,12 +42,10 @@ export default class ImageFile {
|
|||
.filter(`.${viewMode}`)
|
||||
.addClass('active');
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(200, () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(`.view.${viewMode}`, this.file).fadeIn(200);
|
||||
return this.initView(viewMode);
|
||||
});
|
||||
$(`.view:visible:not(.${viewMode})`, this.file).addClass('gl-display-none');
|
||||
$(`.view.${viewMode}`, this.file).removeClass('gl-display-none');
|
||||
|
||||
return this.initView(viewMode);
|
||||
}
|
||||
|
||||
initView(viewMode) {
|
||||
|
@ -120,7 +118,7 @@ export default class ImageFile {
|
|||
return this.requestImageInfo($('img', wrap), (width, height) => {
|
||||
$('.image-info .meta-width', wrap).text(`${width}px`);
|
||||
$('.image-info .meta-height', wrap).text(`${height}px`);
|
||||
return $('.image-info', wrap).removeClass('hide');
|
||||
return $('.image-info', wrap).removeClass('gl-display-none');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -119,20 +119,18 @@ class DueDateSelect {
|
|||
}
|
||||
|
||||
updateIssueBoardIssue() {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.fadeIn();
|
||||
this.$loading.removeClass('gl-display-none');
|
||||
this.$dropdown.trigger('loading.gl.dropdown');
|
||||
this.$selectbox.hide();
|
||||
this.$value.css('display', '');
|
||||
const fadeOutLoader = () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.fadeOut();
|
||||
const hideLoader = () => {
|
||||
this.$loading.addClass('gl-display-none');
|
||||
};
|
||||
|
||||
boardsStore.detail.issue
|
||||
.update(this.$dropdown.attr('data-issue-update'))
|
||||
.then(fadeOutLoader)
|
||||
.catch(fadeOutLoader);
|
||||
.then(hideLoader)
|
||||
.catch(hideLoader);
|
||||
}
|
||||
|
||||
submitSelectedDate(isDropdown) {
|
||||
|
@ -140,8 +138,7 @@ class DueDateSelect {
|
|||
const hasDueDate = this.displayedDate !== __('None');
|
||||
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.removeClass('hidden').fadeIn();
|
||||
this.$loading.removeClass('gl-display-none');
|
||||
|
||||
if (isDropdown) {
|
||||
this.$dropdown.trigger('loading.gl.dropdown');
|
||||
|
@ -164,8 +161,7 @@ class DueDateSelect {
|
|||
}
|
||||
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
return this.$loading.fadeOut();
|
||||
return this.$loading.addClass('gl-display-none');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +207,8 @@ export default class DueDateSelectors {
|
|||
initIssuableSelect() {
|
||||
const $loading = $('.js-issuable-update .due_date')
|
||||
.find('.block-loading')
|
||||
.hide();
|
||||
.removeClass('hidden')
|
||||
.addClass('gl-display-none');
|
||||
|
||||
$('.js-due-date-select').each((i, dropdown) => {
|
||||
const $dropdown = $(dropdown);
|
||||
|
|
|
@ -64,8 +64,7 @@ export default class FilterableList {
|
|||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this.listHolderElement).fadeTo(250, 0.5);
|
||||
$(this.listHolderElement).addClass('gl-opacity-5');
|
||||
|
||||
this.isBusy = true;
|
||||
|
||||
|
@ -99,7 +98,6 @@ export default class FilterableList {
|
|||
|
||||
onFilterComplete() {
|
||||
this.isBusy = false;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this.listHolderElement).fadeTo(250, 1);
|
||||
$(this.listHolderElement).removeClass('gl-opacity-5');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,7 @@ export default class LabelsSelect {
|
|||
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||
const $value = $block.find('.value');
|
||||
const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const $loading = $block.find('.block-loading').addClass('gl-display-none');
|
||||
const fieldName = $dropdown.data('fieldName');
|
||||
let initialSelected = $selectbox
|
||||
.find(`input[name="${$dropdown.data('fieldName')}"]`)
|
||||
|
@ -83,15 +82,13 @@ export default class LabelsSelect {
|
|||
if (!selected.length) {
|
||||
data[abilityName].label_ids = [''];
|
||||
}
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$loading.removeClass('gl-display-none');
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
axios
|
||||
.put(issueUpdateURL, data)
|
||||
.then(({ data }) => {
|
||||
let template;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$selectbox.hide();
|
||||
data.issueUpdateURL = issueUpdateURL;
|
||||
|
@ -340,9 +337,8 @@ export default class LabelsSelect {
|
|||
const { $el, e, isMarking } = clickEvent;
|
||||
const label = clickEvent.selectedObj;
|
||||
|
||||
const fadeOutLoader = () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
const hideLoader = () => {
|
||||
$loading.addClass('gl-display-none');
|
||||
};
|
||||
|
||||
const page = $('body').attr('data-page');
|
||||
|
@ -403,8 +399,7 @@ export default class LabelsSelect {
|
|||
boardsStore.detail.issue.labels = labels;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeIn();
|
||||
$loading.removeClass('gl-display-none');
|
||||
const oldLabels = boardsStore.detail.issue.labels;
|
||||
|
||||
boardsStore.detail.issue
|
||||
|
@ -420,8 +415,8 @@ export default class LabelsSelect {
|
|||
.removeClass('is-active');
|
||||
}
|
||||
})
|
||||
.then(fadeOutLoader)
|
||||
.catch(fadeOutLoader);
|
||||
.then(hideLoader)
|
||||
.catch(hideLoader);
|
||||
} else if (handleClick) {
|
||||
e.preventDefault();
|
||||
handleClick(label);
|
||||
|
|
|
@ -136,10 +136,9 @@ function deferredInitialisation() {
|
|||
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
|
||||
tooltips.dispose(this);
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this)
|
||||
.closest('li')
|
||||
.fadeOut();
|
||||
.addClass('gl-display-none!');
|
||||
});
|
||||
|
||||
$('.js-remove-tr').on('ajax:before', function removeTRAjaxBeforeCallback() {
|
||||
|
@ -147,10 +146,9 @@ function deferredInitialisation() {
|
|||
});
|
||||
|
||||
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this)
|
||||
.closest('tr')
|
||||
.fadeOut();
|
||||
.addClass('gl-display-none!');
|
||||
});
|
||||
|
||||
const glTooltipDelay = localStorage.getItem('gl-tooltip-delay');
|
||||
|
|
|
@ -53,8 +53,7 @@ export default class MilestoneSelect {
|
|||
const $block = $selectBox.closest('.block');
|
||||
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
|
||||
const $value = $block.find('.value');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const $loading = $block.find('.block-loading').addClass('gl-display-none');
|
||||
selectedMilestoneDefault = showAny ? '' : null;
|
||||
selectedMilestoneDefault =
|
||||
showNo && defaultNo ? __('No milestone') : selectedMilestoneDefault;
|
||||
|
@ -255,34 +254,29 @@ export default class MilestoneSelect {
|
|||
}
|
||||
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$loading.removeClass('gl-display-none');
|
||||
|
||||
boardsStore.detail.issue
|
||||
.update($dropdown.attr('data-issue-update'))
|
||||
.then(() => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
})
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
});
|
||||
} else {
|
||||
selected = $selectBox.find('input[type="hidden"]').val();
|
||||
data = {};
|
||||
data[abilityName] = {};
|
||||
data[abilityName].milestone_id = selected != null ? selected : null;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$loading.removeClass('gl-display-none');
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
return axios
|
||||
.put(issueUpdateURL, data)
|
||||
.then(({ data }) => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
$selectBox.hide();
|
||||
$value.css('display', '');
|
||||
if (data.milestone != null) {
|
||||
|
@ -313,8 +307,7 @@ export default class MilestoneSelect {
|
|||
.text(__('None'));
|
||||
})
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -63,8 +63,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
const abilityName = $dropdown.data('abilityName');
|
||||
let $value = $block.find('.value');
|
||||
const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const $loading = $block.find('.block-loading').addClass('gl-display-none');
|
||||
const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
|
||||
let selectedId = $dropdown.data('selected');
|
||||
let assignTo;
|
||||
|
@ -205,16 +204,14 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
const data = {};
|
||||
data[abilityName] = {};
|
||||
data[abilityName].assignee_id = selected != null ? selected : null;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$loading.removeClass('gl-display-none');
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
|
||||
return axios.put(issueURL, data).then(({ data }) => {
|
||||
let user = {};
|
||||
let tooltipTitle = user.name;
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$loading.addClass('gl-display-none');
|
||||
if (data.assignee) {
|
||||
user = {
|
||||
name: data.assignee.name,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
|
||||
@mixin inset-border-1-red-500($important: false) {
|
||||
box-shadow: inset 0 0 0 $gl-border-size-1 $red-500 if-important($important);
|
||||
}
|
||||
|
||||
.timezone-dropdown {
|
||||
.dropdown-menu {
|
||||
@include gl-w-full;
|
||||
}
|
||||
|
||||
.gl-new-dropdown-item-text-primary {
|
||||
@include gl-overflow-hidden;
|
||||
@include gl-text-overflow-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@include gl-bg-gray-10;
|
||||
}
|
||||
|
||||
.invalid-dropdown {
|
||||
.gl-dropdown-toggle {
|
||||
@include inset-border-1-red-500;
|
||||
|
||||
&:hover {
|
||||
@include inset-border-1-red-500(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ module Registrations
|
|||
end
|
||||
|
||||
def update_params
|
||||
params.require(:user).permit(:role, :setup_for_company)
|
||||
params.require(:user).permit(:role, :other_role, :setup_for_company)
|
||||
end
|
||||
|
||||
def requires_confirmation?(user)
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
module PipelineSchedulesHelper
|
||||
def timezone_data
|
||||
ActiveSupport::TimeZone.all.map do |timezone|
|
||||
{
|
||||
name: timezone.name,
|
||||
offset: timezone.now.utc_offset,
|
||||
identifier: timezone.tzinfo.identifier
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TimeZoneHelper
|
||||
def timezone_data
|
||||
ActiveSupport::TimeZone.all.map do |timezone|
|
||||
{
|
||||
identifier: timezone.tzinfo.identifier,
|
||||
name: timezone.name,
|
||||
abbr: timezone.tzinfo.strftime('%Z'),
|
||||
offset: timezone.now.utc_offset,
|
||||
formatted_offset: timezone.now.formatted_offset
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,7 +41,7 @@ module Pages
|
|||
|
||||
def deployment
|
||||
strong_memoize(:deployment) do
|
||||
next unless Feature.enabled?(:pages_serve_from_deployments, project)
|
||||
next unless Feature.enabled?(:pages_serve_from_deployments, project, default_enabled: true)
|
||||
|
||||
project.pages_metadatum.pages_deployment
|
||||
end
|
||||
|
|
|
@ -289,6 +289,7 @@ class User < ApplicationRecord
|
|||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
|
||||
delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
|
||||
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
|
||||
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SystemHooksService
|
||||
BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember].freeze
|
||||
|
||||
def execute_hooks_for(model, event)
|
||||
data = build_event_data(model, event)
|
||||
|
||||
|
@ -20,6 +22,9 @@ class SystemHooksService
|
|||
private
|
||||
|
||||
def build_event_data(model, event)
|
||||
# return entire event data from its builder class, if available.
|
||||
return builder_driven_event_data(model, event) if builder_driven_event_data_available?(model)
|
||||
|
||||
data = {
|
||||
event_name: build_event_name(model, event),
|
||||
created_at: model.created_at&.xmlschema,
|
||||
|
@ -62,8 +67,6 @@ class SystemHooksService
|
|||
old_full_path: model.full_path_before_last_save
|
||||
)
|
||||
end
|
||||
when GroupMember
|
||||
data.merge!(group_member_data(model))
|
||||
end
|
||||
|
||||
data
|
||||
|
@ -75,10 +78,6 @@ class SystemHooksService
|
|||
return "user_add_to_team" if event == :create
|
||||
return "user_remove_from_team" if event == :destroy
|
||||
return "user_update_for_team" if event == :update
|
||||
when GroupMember
|
||||
return 'user_add_to_group' if event == :create
|
||||
return 'user_remove_from_group' if event == :destroy
|
||||
return 'user_update_for_group' if event == :update
|
||||
else
|
||||
"#{model.class.name.downcase}_#{event}"
|
||||
end
|
||||
|
@ -128,19 +127,6 @@ class SystemHooksService
|
|||
}
|
||||
end
|
||||
|
||||
def group_member_data(model)
|
||||
{
|
||||
group_name: model.group.name,
|
||||
group_path: model.group.path,
|
||||
group_id: model.group.id,
|
||||
user_username: model.user.username,
|
||||
user_name: model.user.name,
|
||||
user_email: model.user.email,
|
||||
user_id: model.user.id,
|
||||
group_access: model.human_access
|
||||
}
|
||||
end
|
||||
|
||||
def user_data(model)
|
||||
{
|
||||
name: model.name,
|
||||
|
@ -149,6 +135,17 @@ class SystemHooksService
|
|||
username: model.username
|
||||
}
|
||||
end
|
||||
|
||||
def builder_driven_event_data_available?(model)
|
||||
model.class.in?(BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES)
|
||||
end
|
||||
|
||||
def builder_driven_event_data(model, event)
|
||||
case model
|
||||
when GroupMember
|
||||
Gitlab::HookData::GroupMemberBuilder.new(model).build(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SystemHooksService.prepend_if_ee('EE::SystemHooksService')
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
.wrap
|
||||
.frame.deleted
|
||||
= image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
|
||||
%p.image-info.hide
|
||||
%p.image-info.gl-display-none
|
||||
%span.meta-filesize= number_to_human_size(old_blob.size)
|
||||
|
|
||||
%strong W:
|
||||
|
@ -24,7 +24,7 @@
|
|||
%span.meta-height
|
||||
.wrap
|
||||
= render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
|
||||
%p.image-info.hide
|
||||
%p.image-info.gl-display-none
|
||||
%span.meta-filesize= number_to_human_size(blob.size)
|
||||
|
|
||||
%strong W:
|
||||
|
@ -33,7 +33,7 @@
|
|||
%strong H:
|
||||
%span.meta-height
|
||||
|
||||
.swipe.view.hide
|
||||
.swipe.view.gl-display-none
|
||||
.swipe-frame
|
||||
.frame.deleted.old-diff
|
||||
= image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
|
||||
|
@ -43,7 +43,7 @@
|
|||
%span.top-handle
|
||||
%span.bottom-handle
|
||||
|
||||
.onion-skin.view.hide
|
||||
.onion-skin.view.gl-display-none
|
||||
.onion-skin-frame
|
||||
.frame.deleted
|
||||
= image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
|
||||
|
@ -54,7 +54,7 @@
|
|||
.dragger{ :style => "left: 0px;" }
|
||||
.opaque
|
||||
|
||||
.view-modes.hide
|
||||
.view-modes.gl-display-none
|
||||
%ul.view-modes-menu
|
||||
%li.two-up{ data: { mode: 'two-up' } } 2-up
|
||||
%li.swipe{ data: { mode: 'swipe' } } Swipe
|
||||
|
|
|
@ -14,8 +14,16 @@
|
|||
.row
|
||||
.form-group.col-sm-12
|
||||
= f.label :role, _('Role'), class: 'label-bold'
|
||||
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control', autofocus: true
|
||||
.form-text.gl-text-gray-500.gl-mt-3= _('This will help us personalize your onboarding experience.')
|
||||
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control js-user-role-dropdown', autofocus: true
|
||||
- if Feature.enabled?(:user_other_role_details)
|
||||
.row
|
||||
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
|
||||
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
|
||||
= f.text_field :other_role, class: 'form-control'
|
||||
- else
|
||||
.row
|
||||
.form-group.col-sm-12
|
||||
.form-text.gl-text-gray-500.gl-mt-0.gl-line-height-normal.gl-px-1= _('This will help us personalize your onboarding experience.')
|
||||
= render_if_exists "registrations/welcome/setup_for_company", f: f
|
||||
.row
|
||||
.form-group.col-sm-12.gl-mb-0
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add other role column in user details table
|
||||
merge_request: 45635
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Avoid creating wiki empty repo when not present in export files
|
||||
merge_request: 48890
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable pages_serve_from_deployments FF by default
|
||||
merge_request: 48974
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Upgrade fog-aws to v3.7.0
|
||||
merge_request: 48921
|
||||
author:
|
||||
type: changed
|
|
@ -203,6 +203,7 @@ module Gitlab
|
|||
config.assets.precompile << "page_bundles/wiki.css"
|
||||
config.assets.precompile << "page_bundles/xterm.css"
|
||||
config.assets.precompile << "page_bundles/alert_management_settings.css"
|
||||
config.assets.precompile << "page_bundles/oncall_schedules.css"
|
||||
config.assets.precompile << "lazy_bundles/cropper.css"
|
||||
config.assets.precompile << "lazy_bundles/select2.css"
|
||||
config.assets.precompile << "performance_bar.css"
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/29
|
|||
milestone: '13.6'
|
||||
type: development
|
||||
group: group::Release Management
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: user_other_role_details
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45635
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255170
|
||||
milestone: '13.7'
|
||||
type: development
|
||||
group: group::conversion
|
||||
default_enabled: false
|
|
@ -139,4 +139,12 @@ def warn_or_fail_commits(failed_linters, default_to_fail: true)
|
|||
end
|
||||
end
|
||||
|
||||
lint_commits(git.commits)
|
||||
# As part of https://gitlab.com/groups/gitlab-org/-/epics/4826 we are
|
||||
# vendoring workhorse commits from the stand-alone gitlab-workhorse
|
||||
# repo. There is no point in linting commits that we want to vendor as
|
||||
# is.
|
||||
def workhorse_changes?
|
||||
git.diff.any? { |file| file.path.start_with?('workhorse/') }
|
||||
end
|
||||
|
||||
lint_commits(git.commits) unless workhorse_changes?
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddOtherRoleToUserDetails < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless column_exists?(:user_details, :other_role)
|
||||
with_lock_retries do
|
||||
add_column :user_details, :other_role, :text
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :user_details, :other_role, 100
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :user_details, :other_role
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
70fae11d6a73ea8b2ad75c574716f48e9cc78a58ae23db48e74840646fd46672
|
|
@ -16961,7 +16961,9 @@ CREATE TABLE user_details (
|
|||
bio_html text,
|
||||
cached_markdown_version integer,
|
||||
webauthn_xid text,
|
||||
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100))
|
||||
other_role text,
|
||||
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
|
||||
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE user_details_user_id_seq
|
||||
|
|
|
@ -175,7 +175,7 @@ production:
|
|||
| `password` | The password of the bind user. | no | `'your_great_password'` |
|
||||
| `encryption` | Encryption method. The `method` key is deprecated in favor of `encryption`. | yes | `'start_tls'` or `'simple_tls'` or `'plain'` |
|
||||
| `verify_certificates` | Enables SSL certificate verification if encryption method is `start_tls` or `simple_tls`. Defaults to true. | no | boolean |
|
||||
| `timeout` | Set a timeout, in seconds, for LDAP queries. This helps avoid blocking a request if the LDAP server becomes unresponsive. A value of 0 means there is no timeout. | no | `10` or `30` |
|
||||
| `timeout` | Set a timeout, in seconds, for LDAP queries. This helps avoid blocking a request if the LDAP server becomes unresponsive. A value of `0` means there is no timeout. (default: `10`) | no | `10` or `30` |
|
||||
| `active_directory` | This setting specifies if LDAP server is Active Directory LDAP server. For non-AD servers it skips the AD specific queries. If your LDAP server is not AD, set this to false. | no | boolean |
|
||||
| `allow_username_or_email_login` | If enabled, GitLab ignores everything after the first `@` in the LDAP username submitted by the user on sign-in. If you are using `uid: 'userPrincipalName'` on ActiveDirectory you need to disable this setting, because the userPrincipalName contains an `@`. | no | boolean |
|
||||
| `block_auto_created_users` | To maintain tight control over the number of billable users on your GitLab installation, enable this setting to keep new users blocked until they have been cleared by an administrator (default: false). | no | boolean |
|
||||
|
|
|
@ -135,6 +135,7 @@ from:
|
|||
- [Wikis development guide](wikis.md)
|
||||
- [Newlines style guide](newlines_styleguide.md)
|
||||
- [Image scaling guide](image_scaling.md)
|
||||
- [Export to CSV](export_csv.md)
|
||||
|
||||
## Performance guides
|
||||
|
||||
|
|
|
@ -395,7 +395,9 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
|
||||
API requests are protected by checking for a valid CSRF token.
|
||||
|
||||
In order to be able to increment the values the related feature `usage_data<event_name>` should be enabled.
|
||||
In order to increment the values, the related feature `usage_data_<event_name>` should be
|
||||
set to `default_enabled: true`. For more information, see
|
||||
[Feature flags in development of GitLab](../feature_flags/index.md).
|
||||
|
||||
```plaintext
|
||||
POST /usage_data/increment_unique_users
|
||||
|
@ -418,7 +420,10 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
|
||||
Example usage for an existing event already defined in [known events](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events/):
|
||||
|
||||
Note that `usage_data_api` and `usage_data_#{event_name}` should be enabled in order to be able to track events
|
||||
Usage Data API is behind `usage_data_api` feature flag which, as of GitLab 13.7, is
|
||||
now set to `default_enabled: true`.
|
||||
|
||||
Each event tracked using Usage Data API is behind a feature flag `usage_data_#{event_name}` which should be `default_enabled: true`
|
||||
|
||||
```javascript
|
||||
import api from '~/api';
|
||||
|
|
202
doc/redirects.sh
202
doc/redirects.sh
|
@ -1,202 +0,0 @@
|
|||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> install/redis.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> install/google-protobuf.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> install/ldap.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> license/README.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/welcome_message.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/issue_closing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/help_message.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/system_header_and_footer_messages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/branded_page_and_email_header.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/issue_and_merge_request_template.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/new_project_page.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/branded_login_page.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/libravatar.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> customization/favicon.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/ux_guide/users.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/prometheus.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/documentation/feature-change-workflow.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/documentation/improvement-workflow.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/documentation/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/rolling_out_changes_using_feature_flags.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/event_tracking/backend.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/event_tracking/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/event_tracking/frontend.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/product_analytics/event_dictionary.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/product_analytics/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/testing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/sidekiq_debugging.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/doc_styleguide.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/feature_flags.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/frontend.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/telemetry/event_dictionary.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/telemetry/snowplow.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/telemetry/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/telemetry/usage_ping.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/cycle_analytics.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/fe_guide/event_tracking.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/fe_guide/style_guide_js.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/fe_guide/testing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/fe_guide/style_guide_scss.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/i18n_guide.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/new_fe_guide/development/testing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/new_fe_guide/style/javascript.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/new_fe_guide/style/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/new_fe_guide/style/prettier.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> development/new_fe_guide/style/html.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/environments.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/autodeploy/quick_start_guide.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/autodeploy/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/pipelines.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/multi_project_pipeline_graphs.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/junit_test_reports.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/permissions/README.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/build_artifacts/README.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/code_climate.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/dependency_scanning.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/code_quality.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/license_management.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/sast.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/browser_performance.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/dast.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/container_scanning.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/examples/sast_docker.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/jenkins/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> ci/services/docker-services.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> markdown/markdown.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> integration/jira.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> integration/slack.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> integration/ldap.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> integration/chat_commands.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> integration/crowd.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/admin_area/user_cohorts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/admin_area/monitoring/dev_ops_report.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/admin_area/analytics/convdev.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/group/security_dashboard/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/group/dependency_proxy/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/incident_management/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/application_security/compliance_dashboard/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/application_security/license_compliance/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/application_security/license_management/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/status_page/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/releases.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/container_registry.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/ci_cd_for_external_repo.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/clusters/eks_and_gitlab/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pipelines/settings.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pipelines/job_artifacts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pipelines/schedules.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/merge_request_discussion_resolution.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/dependency_scanning.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/license_management.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/sast.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/merge_when_build_succeeds.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/code_quality_diff.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/maintainer_access.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/dast.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/container_scanning.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests/sast_docker.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/builds/artifacts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/merge_requests.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/tracing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/feature_flags.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/linking_to_an_external_dashboard.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/error_tracking.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/alert_management.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/operations/dashboard_settings.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/integrations/kubernetes.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/integrations/prometheus_units.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/integrations/project_services.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/integrations/generic_alerts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/integrations/prometheus_library/metrics.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/maven_packages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/packages/npm_registry.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/packages/maven.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/packages/maven_packages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/packages/maven_repository.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/milestones/burndown_charts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/gpg_signed_commits/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/cycle_analytics.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/import/tfs.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/moving_issues.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/closing_issues.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/create_new_issue.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/automatic_issue_closing.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/similar_issues.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/issues/deleting_issues.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started_part_four.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started_part_three.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started_part_two.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started/new_or_existing_website.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started/fork_sample_project.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/pages/getting_started/pages_bundled_template.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/slash_commands.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/project/security_dashboard.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/profile/account/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/account/two_factor_authentication.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/account/security.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> user/analytics/cycle_analytics.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> university/training/topics/explore_gitlab.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> university/high-availability/aws/README.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> permissions/permissions.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/cleaning_up_redis_sessions.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/sidekiq_memory_killer.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/incident_management/alert_details.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/incident_management/generic_alerts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/moving_repositories.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> operations/unicorn.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> raketasks/check.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> raketasks/maintenance.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> api/deploy_key_multiple_projects.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> api/license_templates.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> api/build_triggers.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> api/builds.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> gitlab-basics/add-merge-request.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> gitlab-basics/basic-git-commands.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> gitlab-basics/create-issue.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> gitlab-basics/add-image.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> gitlab-basics/create-group.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/npm_registry.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/operations.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/container_registry.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/ldap-ee.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/how_to_configure_ldap_gitlab_ee/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/google_secure_ldap.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/ldap-troubleshooting.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/ldap.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/auth/how_to_configure_ldap_gitlab_ce/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/geo/replication/high_availability.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/geo/replication/external_database.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/geo/replication/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/geo/replication/database.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/geo/disaster_recovery/promotion_runbook.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/build_artifacts.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/operations/speed_up_ssh.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/custom_hooks.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/scaling/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/maven_packages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/plugins.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/availability/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/lfs/migrate_from_git_annex_to_git_lfs.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/lfs/lfs_administration.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/lfs/manage_large_binaries_with_git_lfs.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/job_traces.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/maven_repository.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/packages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/repository_storages.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/monitoring/gitlab_instance_administration_project/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/monitoring/prometheus/gitlab_monitor_exporter.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/monitoring/performance/prometheus.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/monitoring/performance/introduction.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> administration/dependency_proxy.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> telemetry/snowplow.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> telemetry/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> monitoring/health_check.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> monitoring/performance/grafana_configuration.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> monitoring/performance/introduction.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> monitoring/performance/gitlab_configuration.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> topics/autodevops/upgrading_chart.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> topics/git/migrate_to_git_lfs/index.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> analytics/README.md
|
||||
echo '\n<!-- This redirect file can be deleted after February 1, 2021. -->\n<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->' >> analytics/contribution_analytics.md
|
|
@ -21,6 +21,13 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def timestamps_data
|
||||
{
|
||||
created_at: object.created_at&.xmlschema,
|
||||
updated_at: object.updated_at&.xmlschema
|
||||
}
|
||||
end
|
||||
|
||||
def absolute_image_urls(markdown_text)
|
||||
return markdown_text unless markdown_text.present?
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module HookData
|
||||
class GroupMemberBuilder < BaseBuilder
|
||||
alias_method :group_member, :object
|
||||
|
||||
# Sample data
|
||||
|
||||
# {
|
||||
# :event_name=>"user_add_to_group",
|
||||
# :group_name=>"GitLab group",
|
||||
# :group_path=>"gitlab",
|
||||
# :group_id=>1,
|
||||
# :user_username=>"robert",
|
||||
# :user_name=>"Robert Mills",
|
||||
# :user_email=>"robert@example.com",
|
||||
# :user_id=>14,
|
||||
# :group_access=>"Guest",
|
||||
# :created_at=>"2020-11-04T10:12:10Z",
|
||||
# :updated_at=>"2020-11-04T10:12:10Z",
|
||||
# }
|
||||
|
||||
def build(event)
|
||||
[
|
||||
timestamps_data,
|
||||
group_member_data,
|
||||
event_data(event)
|
||||
].reduce(:merge)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group_member_data
|
||||
{
|
||||
group_name: group_member.group.name,
|
||||
group_path: group_member.group.path,
|
||||
group_id: group_member.group.id,
|
||||
user_username: group_member.user.username,
|
||||
user_name: group_member.user.name,
|
||||
user_email: group_member.user.email,
|
||||
user_id: group_member.user.id,
|
||||
group_access: group_member.human_access
|
||||
}
|
||||
end
|
||||
|
||||
def event_data(event)
|
||||
event_name = case event
|
||||
when :create
|
||||
'user_add_to_group'
|
||||
when :destroy
|
||||
'user_remove_from_group'
|
||||
when :update
|
||||
'user_update_for_group'
|
||||
end
|
||||
|
||||
{ event_name: event_name }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::HookData::GroupMemberBuilder.prepend_if_ee('EE::Gitlab::HookData::GroupMemberBuilder')
|
|
@ -79,10 +79,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def wiki_restorer
|
||||
Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
|
||||
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
|
||||
shared: shared,
|
||||
project: ProjectWiki.new(project),
|
||||
wiki_enabled: project.wiki_enabled?)
|
||||
project: ProjectWiki.new(project))
|
||||
end
|
||||
|
||||
def design_repo_restorer
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ImportExport
|
||||
class WikiRestorer < RepoRestorer
|
||||
def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
|
||||
super(project: project, shared: shared, path_to_bundle: path_to_bundle)
|
||||
|
||||
@project = project
|
||||
@wiki_enabled = wiki_enabled
|
||||
end
|
||||
|
||||
def restore
|
||||
project.wiki if create_empty_wiki?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :project, :wiki_enabled
|
||||
|
||||
def create_empty_wiki?
|
||||
!File.exist?(path_to_bundle) && wiki_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4881,6 +4881,9 @@ msgstr ""
|
|||
msgid "Can't apply this suggestion."
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't be empty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't create snippet: %{err}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9348,6 +9351,9 @@ msgstr ""
|
|||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19033,12 +19039,24 @@ msgstr ""
|
|||
msgid "OnCallSchedules|Add a schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Add schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Create on-call schedules in GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Failed to add schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Route alerts directly to specific members of your team"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Select timezone"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28436,6 +28454,9 @@ msgstr ""
|
|||
msgid "Timeout connecting to the Google API. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Timezone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Time|hr"
|
||||
msgid_plural "Time|hrs"
|
||||
msgstr[0] ""
|
||||
|
@ -30649,6 +30670,9 @@ msgstr ""
|
|||
msgid "What is squashing?"
|
||||
msgstr ""
|
||||
|
||||
msgid "What is your job title? (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "What's new at GitLab"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
WORKHORSE_DIR=workhorse/
|
||||
WORKHORSE_REF="v$(cat GITLAB_WORKHORSE_VERSION)"
|
||||
|
||||
if [ $# -gt 1 ] || ([ $# = 1 ] && [ x$1 != xcheck ]); then
|
||||
echo "Usage: update-workhorse [check]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
clean="$(git status --porcelain)"
|
||||
if [ -n "$clean" ] ; then
|
||||
echo 'error: working directory is not clean:'
|
||||
echo "$clean"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch https://gitlab.com/gitlab-org/gitlab-workhorse.git "$WORKHORSE_REF"
|
||||
git rm -rf --quiet -- "$WORKHORSE_DIR"
|
||||
git read-tree --prefix="$WORKHORSE_DIR" -u FETCH_HEAD
|
||||
|
||||
status="$(git status --porcelain)"
|
||||
|
||||
if [ x$1 = xcheck ]; then
|
||||
if [ -n "$status" ]; then
|
||||
cat <<MSG
|
||||
error: $WORKHORSE_DIR does not match $WORKHORSE_REF
|
||||
|
||||
During the transition period of https://gitlab.com/groups/gitlab-org/-/epics/4826,
|
||||
the workhorse/ directory in this repository is read-only. To make changes:
|
||||
|
||||
1. Submit a MR to https://gitlab.com/gitlab-org/gitlab-workhorse
|
||||
2. Once your MR is merged, have a new gitlab-workhorse tag made
|
||||
by a maintainer
|
||||
3. Update the GITLAB_WORKHORSE_VERSION file in this repository
|
||||
4. Run scripts/update-workhorse to update the workhorse/ directory
|
||||
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$status" ]; then
|
||||
echo "warn: $WORKHORSE_DIR is already up to date, exiting without commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tree=$(git write-tree)
|
||||
msg="Update vendored workhorse to $WORKHORSE_REF"
|
||||
commit=$(git commit-tree -p HEAD -p FETCH_HEAD^{commit} -m "$msg" "$tree")
|
||||
git update-ref HEAD "$commit"
|
||||
git log -1
|
|
@ -341,7 +341,7 @@ RSpec.describe Admin::ClustersController do
|
|||
expect { post_create_aws }.not_to change { Clusters::Cluster.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(response.content_type).to eq('application/json')
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(response.body).to include('is invalid')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe Boards::ListsController do
|
|||
read_board_list user: user, board: board
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'returns a list of board lists' do
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe Groups::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to render_template :index
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
@ -36,7 +36,7 @@ RSpec.describe Groups::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe Groups::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to render_template :index
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -81,7 +81,7 @@ RSpec.describe Groups::BoardsController do
|
|||
list_boards format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -103,7 +103,7 @@ RSpec.describe Groups::BoardsController do
|
|||
expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1)
|
||||
|
||||
expect(response).to render_template :show
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
@ -118,7 +118,7 @@ RSpec.describe Groups::BoardsController do
|
|||
read_board board: board
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -131,7 +131,7 @@ RSpec.describe Groups::BoardsController do
|
|||
expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0)
|
||||
|
||||
expect(response).to render_template :show
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -157,7 +157,7 @@ RSpec.describe Groups::BoardsController do
|
|||
read_board board: board, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -476,7 +476,7 @@ RSpec.describe Groups::ClustersController do
|
|||
expect { post_create_aws }.not_to change { Clusters::Cluster.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(response.content_type).to eq('application/json')
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(response.body).to include('is invalid')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -177,7 +177,7 @@ RSpec.describe Groups::MilestonesController do
|
|||
expect(milestones.count).to eq(3)
|
||||
expect(milestones.collect { |m| m['title'] }).to match_array(['same name', 'same name', 'group milestone'])
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
context 'with subgroup milestones' do
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe Groups::ReleasesController do
|
|||
end
|
||||
|
||||
it 'returns an application/json content_type' do
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it 'returns OK' do
|
||||
|
|
|
@ -34,14 +34,14 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'text/plain'
|
||||
expect(response.media_type).to eq 'text/plain'
|
||||
end
|
||||
|
||||
it 'supports passing the token in query params' do
|
||||
get :index, params: { token: token }
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'text/plain'
|
||||
expect(response.media_type).to eq 'text/plain'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -55,14 +55,14 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'text/plain'
|
||||
expect(response.media_type).to eq 'text/plain'
|
||||
end
|
||||
|
||||
it 'supports successful json response' do
|
||||
get :index, format: :json
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['healthy']).to be true
|
||||
end
|
||||
|
||||
|
@ -70,7 +70,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index, format: :xml
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'application/xml'
|
||||
expect(response.media_type).to eq 'application/xml'
|
||||
expect(xml_response['healthy']).to be true
|
||||
end
|
||||
|
||||
|
@ -78,7 +78,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index, params: { checks: 'email' }, format: :json
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['healthy']).to be true
|
||||
end
|
||||
end
|
||||
|
@ -102,7 +102,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:internal_server_error)
|
||||
expect(response.content_type).to eq 'text/plain'
|
||||
expect(response.media_type).to eq 'text/plain'
|
||||
expect(response.body).to include('The server is on fire')
|
||||
end
|
||||
|
||||
|
@ -110,7 +110,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:internal_server_error)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['healthy']).to be false
|
||||
expect(json_response['message']).to include('The server is on fire')
|
||||
end
|
||||
|
@ -119,7 +119,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index, format: :xml
|
||||
|
||||
expect(response).to have_gitlab_http_status(:internal_server_error)
|
||||
expect(response.content_type).to eq 'application/xml'
|
||||
expect(response.media_type).to eq 'application/xml'
|
||||
expect(xml_response['healthy']).to be false
|
||||
expect(xml_response['message']).to include('The server is on fire')
|
||||
end
|
||||
|
@ -128,7 +128,7 @@ RSpec.describe HealthCheckController, :request_store do
|
|||
get :index, params: { checks: 'email' }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:internal_server_error)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['healthy']).to be false
|
||||
expect(json_response['message']).to include('Email is on fire')
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ RSpec.describe Profiles::KeysController do
|
|||
end
|
||||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
expect(response.media_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -84,7 +84,7 @@ RSpec.describe Profiles::KeysController do
|
|||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
expect(response.media_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -119,7 +119,7 @@ RSpec.describe Profiles::KeysController do
|
|||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
expect(response.media_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ RSpec.describe Projects::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to render_template :index
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
@ -41,7 +41,7 @@ RSpec.describe Projects::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -57,7 +57,7 @@ RSpec.describe Projects::BoardsController do
|
|||
list_boards
|
||||
|
||||
expect(response).to render_template :index
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ RSpec.describe Projects::BoardsController do
|
|||
list_boards format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -127,7 +127,7 @@ RSpec.describe Projects::BoardsController do
|
|||
expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1)
|
||||
|
||||
expect(response).to render_template :show
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
|
@ -141,7 +141,7 @@ RSpec.describe Projects::BoardsController do
|
|||
read_board board: board
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -154,7 +154,7 @@ RSpec.describe Projects::BoardsController do
|
|||
expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0)
|
||||
|
||||
expect(response).to render_template :show
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -179,7 +179,7 @@ RSpec.describe Projects::BoardsController do
|
|||
read_board board: board, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,7 +82,7 @@ RSpec.describe Projects::Ci::LintsController do
|
|||
end
|
||||
|
||||
it 'renders json' do
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(parsed_body).to include('errors', 'warnings', 'jobs', 'valid')
|
||||
expect(parsed_body).to match_schema('entities/lint_result_entity')
|
||||
end
|
||||
|
|
|
@ -500,7 +500,7 @@ RSpec.describe Projects::ClustersController do
|
|||
expect { post_create_aws }.not_to change { Clusters::Cluster.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(response.content_type).to eq('application/json')
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(response.body).to include('is invalid')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -80,7 +80,7 @@ RSpec.describe Projects::CommitsController do
|
|||
|
||||
it "renders as atom" do
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq('application/atom+xml')
|
||||
expect(response.media_type).to eq('application/atom+xml')
|
||||
end
|
||||
|
||||
it 'renders summary with type=html' do
|
||||
|
@ -105,7 +105,7 @@ RSpec.describe Projects::CommitsController do
|
|||
|
||||
it "renders as HTML" do
|
||||
expect(response).to be_successful
|
||||
expect(response.content_type).to eq('text/html')
|
||||
expect(response.media_type).to eq('text/html')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,14 +33,14 @@ RSpec.describe Projects::MilestonesController do
|
|||
view_milestone
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
it 'returns milestone json' do
|
||||
view_milestone format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -189,7 +189,7 @@ RSpec.describe Projects::MilestonesController do
|
|||
get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
|
||||
expect(json_response['html']).not_to include(label.title)
|
||||
end
|
||||
|
@ -200,7 +200,7 @@ RSpec.describe Projects::MilestonesController do
|
|||
get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
|
||||
expect(json_response['html']).to include(label.title)
|
||||
end
|
||||
|
@ -262,7 +262,7 @@ RSpec.describe Projects::MilestonesController do
|
|||
get :participants, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['html']).to include(issue_assignee.name)
|
||||
end
|
||||
end
|
||||
|
@ -277,7 +277,7 @@ RSpec.describe Projects::MilestonesController do
|
|||
get :participants, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(json_response['html']).not_to include(issue_assignee.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,7 +83,7 @@ RSpec.describe Projects::ReleasesController do
|
|||
let(:format) { :html }
|
||||
|
||||
it 'returns a text/html content_type' do
|
||||
expect(response.content_type).to eq 'text/html'
|
||||
expect(response.media_type).to eq 'text/html'
|
||||
end
|
||||
|
||||
it_behaves_like 'common access controls'
|
||||
|
@ -101,7 +101,7 @@ RSpec.describe Projects::ReleasesController do
|
|||
let(:format) { :json }
|
||||
|
||||
it 'returns an application/json content_type' do
|
||||
expect(response.content_type).to eq 'application/json'
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
end
|
||||
|
||||
it "returns the project's releases as JSON, ordered by released_at" do
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
|
||||
include JavaScriptFixturesHelpers
|
||||
include Ci::PipelineSchedulesHelper
|
||||
include TimeZoneHelper
|
||||
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
|
||||
|
@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
|
|||
end
|
||||
end
|
||||
|
||||
describe Ci::PipelineSchedulesHelper, '(JavaScript fixtures)' do
|
||||
describe TimeZoneHelper, '(JavaScript fixtures)' do
|
||||
let(:response) { timezone_data.to_json }
|
||||
|
||||
it 'api/freeze-periods/timezone_data.json' do
|
||||
# Looks empty but does things
|
||||
# More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38525/diffs#note_391048415
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ RSpec.describe Mutations::Issues::Update do
|
|||
}.merge(expected_attributes)
|
||||
end
|
||||
|
||||
subject { mutation.resolve(mutation_params) }
|
||||
subject { mutation.resolve(**mutation_params) }
|
||||
|
||||
it_behaves_like 'permission level for issue mutation is correctly verified'
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::PipelineSchedulesHelper, :aggregate_failures do
|
||||
describe '#timezone_data' do
|
||||
subject { helper.timezone_data }
|
||||
|
||||
it 'matches schema' do
|
||||
expect(subject).not_to be_empty
|
||||
subject.each_with_index do |timzone_hash, i|
|
||||
expect(timzone_hash.keys).to contain_exactly(:name, :offset, :identifier), "Failed at index #{i}"
|
||||
end
|
||||
end
|
||||
|
||||
it 'formats for display' do
|
||||
first_timezone = ActiveSupport::TimeZone.all[0]
|
||||
|
||||
expect(subject[0][:name]).to eq(first_timezone.name)
|
||||
expect(subject[0][:offset]).to eq(first_timezone.now.utc_offset)
|
||||
expect(subject[0][:identifier]).to eq(first_timezone.tzinfo.identifier)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe TimeZoneHelper, :aggregate_failures do
|
||||
describe '#timezone_data' do
|
||||
subject(:timezone_data) { helper.timezone_data }
|
||||
|
||||
it 'matches schema' do
|
||||
expect(timezone_data).not_to be_empty
|
||||
|
||||
timezone_data.each_with_index do |timezone_hash, i|
|
||||
expect(timezone_hash.keys).to contain_exactly(
|
||||
:identifier,
|
||||
:name,
|
||||
:abbr,
|
||||
:offset,
|
||||
:formatted_offset
|
||||
), "Failed at index #{i}"
|
||||
end
|
||||
end
|
||||
|
||||
it 'formats for display' do
|
||||
tz = ActiveSupport::TimeZone.all[0]
|
||||
|
||||
expect(timezone_data[0]).to eq(
|
||||
identifier: tz.tzinfo.identifier,
|
||||
name: tz.name,
|
||||
abbr: tz.tzinfo.strftime('%Z'),
|
||||
offset: tz.now.utc_offset,
|
||||
formatted_offset: tz.now.formatted_offset
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -242,7 +242,9 @@ RSpec.describe Backup::Repositories do
|
|||
|
||||
# 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo
|
||||
expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args|
|
||||
repository = method.call(*original_args)
|
||||
full_path, container, kwargs = original_args
|
||||
|
||||
repository = method.call(full_path, container, **kwargs)
|
||||
|
||||
expect(repository).to receive(:remove)
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::HookData::GroupMemberBuilder do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_member) { create(:group_member, :developer, group: group) }
|
||||
|
||||
describe '#build' do
|
||||
let(:data) { described_class.new(group_member).build(event) }
|
||||
let(:event_name) { data[:event_name] }
|
||||
let(:attributes) do
|
||||
[
|
||||
:event_name, :created_at, :updated_at, :group_name, :group_path,
|
||||
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
|
||||
]
|
||||
end
|
||||
|
||||
context 'data' do
|
||||
shared_examples_for 'includes the required attributes' do
|
||||
it 'includes the required attributes' do
|
||||
expect(data).to include(*attributes)
|
||||
|
||||
expect(data[:group_name]).to eq(group.name)
|
||||
expect(data[:group_path]).to eq(group.path)
|
||||
expect(data[:group_id]).to eq(group.id)
|
||||
expect(data[:user_username]).to eq(group_member.user.username)
|
||||
expect(data[:user_name]).to eq(group_member.user.name)
|
||||
expect(data[:user_email]).to eq(group_member.user.email)
|
||||
expect(data[:user_id]).to eq(group_member.user.id)
|
||||
expect(data[:group_access]).to eq('Developer')
|
||||
expect(data[:created_at]).to eq(group_member.created_at&.xmlschema)
|
||||
expect(data[:updated_at]).to eq(group_member.updated_at&.xmlschema)
|
||||
end
|
||||
end
|
||||
|
||||
context 'on create' do
|
||||
let(:event) { :create }
|
||||
|
||||
it { expect(event_name).to eq('user_add_to_group') }
|
||||
it_behaves_like 'includes the required attributes'
|
||||
end
|
||||
|
||||
context 'on update' do
|
||||
let(:event) { :update }
|
||||
|
||||
it { expect(event_name).to eq('user_update_for_group') }
|
||||
it_behaves_like 'includes the required attributes'
|
||||
end
|
||||
|
||||
context 'on destroy' do
|
||||
let(:event) { :destroy }
|
||||
|
||||
it { expect(event_name).to eq('user_remove_from_group') }
|
||||
it_behaves_like 'includes the required attributes'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do
|
|||
[
|
||||
Gitlab::ImportExport::AvatarRestorer,
|
||||
Gitlab::ImportExport::RepoRestorer,
|
||||
Gitlab::ImportExport::WikiRestorer,
|
||||
Gitlab::ImportExport::UploadsRestorer,
|
||||
Gitlab::ImportExport::LfsRestorer,
|
||||
Gitlab::ImportExport::StatisticsRestorer,
|
||||
|
@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do
|
|||
end
|
||||
end
|
||||
|
||||
it 'calls RepoRestorer with project and wiki' do
|
||||
wiki_repo_path = File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
|
||||
repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
|
||||
restorer = double(Gitlab::ImportExport::RepoRestorer)
|
||||
|
||||
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
|
||||
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
|
||||
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
|
||||
|
||||
expect(restorer).to receive(:restore).and_return(true).twice
|
||||
|
||||
importer.execute
|
||||
end
|
||||
|
||||
context 'with sample_data_template' do
|
||||
it 'initializes the Sample::TreeRestorer' do
|
||||
project.create_or_update_import_data(data: { sample_data: true })
|
||||
|
|
|
@ -5,35 +5,42 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::ImportExport::RepoRestorer do
|
||||
include GitHelpers
|
||||
|
||||
let_it_be(:project_with_repo) do
|
||||
create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p|
|
||||
p.wiki.create_page('page', 'foobar', :markdown, 'created page')
|
||||
end
|
||||
end
|
||||
|
||||
let!(:project) { create(:project) }
|
||||
|
||||
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
|
||||
let(:shared) { project.import_export_shared }
|
||||
|
||||
before do
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
|
||||
bundler.save
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(export_path)
|
||||
end
|
||||
|
||||
describe 'bundle a project Git repo' do
|
||||
let(:user) { create(:user) }
|
||||
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
|
||||
let!(:project) { create(:project) }
|
||||
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
|
||||
let(:shared) { project.import_export_shared }
|
||||
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
|
||||
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
|
||||
|
||||
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::ImportExport) do |instance|
|
||||
allow(instance).to receive(:storage_path).and_return(export_path)
|
||||
end
|
||||
|
||||
bundler.save
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(export_path)
|
||||
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
||||
FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
|
||||
FileUtils.rm_rf(project.repository.path_to_repo)
|
||||
end
|
||||
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
|
||||
end
|
||||
|
||||
it 'restores the repo successfully' do
|
||||
expect(project.repository.exists?).to be false
|
||||
expect(subject.restore).to be_truthy
|
||||
|
||||
expect(project.repository.empty?).to be false
|
||||
end
|
||||
|
||||
context 'when the repository already exists' do
|
||||
|
@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'restore a wiki Git repo' do
|
||||
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
|
||||
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
|
||||
|
||||
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
|
||||
|
||||
after do
|
||||
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
|
||||
end
|
||||
|
||||
it 'restores the wiki repo successfully' do
|
||||
expect(project.wiki_repository_exists?).to be false
|
||||
|
||||
subject.restore
|
||||
project.wiki.repository.expire_status_cache
|
||||
|
||||
expect(project.wiki_repository_exists?).to be true
|
||||
end
|
||||
|
||||
describe 'no wiki in the bundle' do
|
||||
let!(:project_without_wiki) { create(:project) }
|
||||
|
||||
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
|
||||
|
||||
it 'does not creates an empty wiki' do
|
||||
expect(subject.restore).to be true
|
||||
expect(project.wiki_repository_exists?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::ImportExport::WikiRestorer do
|
||||
describe 'restore a wiki Git repo' do
|
||||
let!(:project_with_wiki) { create(:project, :wiki_repo) }
|
||||
let!(:project_without_wiki) { create(:project) }
|
||||
let!(:project) { create(:project) }
|
||||
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
|
||||
let(:shared) { project.import_export_shared }
|
||||
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) }
|
||||
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
|
||||
let(:restorer) do
|
||||
described_class.new(path_to_bundle: bundle_path,
|
||||
shared: shared,
|
||||
project: project.wiki,
|
||||
wiki_enabled: true)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
|
||||
|
||||
bundler.save
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(export_path)
|
||||
Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
|
||||
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
|
||||
end
|
||||
|
||||
it 'restores the wiki repo successfully' do
|
||||
expect(restorer.restore).to be true
|
||||
end
|
||||
|
||||
describe "no wiki in the bundle" do
|
||||
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
|
||||
|
||||
it 'creates an empty wiki' do
|
||||
expect(restorer.restore).to be true
|
||||
|
||||
expect(project.wiki_repository_exists?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -264,7 +264,7 @@ RSpec.describe API::GroupImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
end
|
||||
|
||||
it 'rejects requests that bypassed gitlab-workhorse' do
|
||||
|
@ -285,7 +285,7 @@ RSpec.describe API::GroupImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response).not_to have_key('TempPath')
|
||||
expect(json_response['RemoteObject']).to have_key('ID')
|
||||
expect(json_response['RemoteObject']).to have_key('GetURL')
|
||||
|
@ -304,7 +304,7 @@ RSpec.describe API::GroupImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
|
||||
expect(json_response['RemoteObject']).to be_nil
|
||||
end
|
||||
|
|
|
@ -303,7 +303,7 @@ RSpec.describe API::ProjectImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
|
||||
end
|
||||
|
||||
|
@ -325,7 +325,7 @@ RSpec.describe API::ProjectImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response).not_to have_key('TempPath')
|
||||
expect(json_response['RemoteObject']).to have_key('ID')
|
||||
expect(json_response['RemoteObject']).to have_key('GetURL')
|
||||
|
@ -344,7 +344,7 @@ RSpec.describe API::ProjectImport do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
|
||||
expect(json_response['RemoteObject']).to be_nil
|
||||
end
|
||||
|
|
|
@ -12,23 +12,23 @@ RSpec.describe Members::ApproveAccessRequestService do
|
|||
|
||||
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { described_class.new(current_user).execute(access_requester, **opts) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
|
||||
it 'raises Gitlab::Access::AccessDeniedError' do
|
||||
expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
|
||||
expect { described_class.new(current_user).execute(access_requester, **opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a service approving an access request' do
|
||||
it 'succeeds' do
|
||||
expect { described_class.new(current_user).execute(access_requester, opts) }.to change { source.requesters.count }.by(-1)
|
||||
expect { described_class.new(current_user).execute(access_requester, **opts) }.to change { source.requesters.count }.by(-1)
|
||||
end
|
||||
|
||||
it 'returns a <Source>Member' do
|
||||
member = described_class.new(current_user).execute(access_requester, opts)
|
||||
member = described_class.new(current_user).execute(access_requester, **opts)
|
||||
|
||||
expect(member).to be_a "#{source.class}Member".constantize
|
||||
expect(member.requested_at).to be_nil
|
||||
|
@ -36,7 +36,7 @@ RSpec.describe Members::ApproveAccessRequestService do
|
|||
|
||||
context 'with a custom access level' do
|
||||
it 'returns a ProjectMember with the custom access level' do
|
||||
member = described_class.new(current_user, access_level: Gitlab::Access::MAINTAINER).execute(access_requester, opts)
|
||||
member = described_class.new(current_user, access_level: Gitlab::Access::MAINTAINER).execute(access_requester, **opts)
|
||||
|
||||
expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
|
|
|
@ -159,9 +159,6 @@ RSpec.describe SystemHooksService do
|
|||
it { expect(event_name(group, :create)).to eq 'group_create' }
|
||||
it { expect(event_name(group, :destroy)).to eq 'group_destroy' }
|
||||
it { expect(event_name(group, :rename)).to eq 'group_rename' }
|
||||
it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' }
|
||||
it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' }
|
||||
it { expect(event_name(group_member, :update)).to eq 'user_update_for_group' }
|
||||
end
|
||||
|
||||
def event_data(*args)
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.shared_examples 'handle uploads authorize request' do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
|
||||
end
|
||||
|
||||
|
@ -30,7 +30,7 @@ RSpec.shared_examples 'handle uploads authorize request' do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response).not_to have_key('TempPath')
|
||||
expect(json_response['RemoteObject']).to have_key('ID')
|
||||
expect(json_response['RemoteObject']).to have_key('GetURL')
|
||||
|
@ -49,7 +49,7 @@ RSpec.shared_examples 'handle uploads authorize request' do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
|
||||
expect(json_response['RemoteObject']).to be_nil
|
||||
end
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
testdata/data
|
||||
testdata/scratch
|
||||
testdata/public
|
||||
/gitlab-workhorse
|
||||
/gitlab-resize-image
|
||||
/gitlab-zip-cat
|
||||
/gitlab-zip-metadata
|
||||
/_build
|
||||
coverage.html
|
||||
/*.toml
|
|
@ -0,0 +1,83 @@
|
|||
workflow:
|
||||
rules: &workflow_rules
|
||||
# For merge requests, create a pipeline.
|
||||
- if: '$CI_MERGE_REQUEST_IID'
|
||||
# For `master` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
|
||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
# For tags, create a pipeline.
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
# For stable branches, create a pipeline.
|
||||
- if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable$/'
|
||||
|
||||
default:
|
||||
image: golang:1.13
|
||||
tags:
|
||||
- gitlab-org
|
||||
|
||||
# Disable DIND for SAST because we need to execute a before_script in the gosec-sast job
|
||||
variables:
|
||||
SAST_DISABLE_DIND: "true"
|
||||
|
||||
verify:
|
||||
script:
|
||||
- make verify
|
||||
|
||||
changelog:
|
||||
script:
|
||||
- _support/check_changelog.sh
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_IID'
|
||||
|
||||
.test:
|
||||
services:
|
||||
- name: registry.gitlab.com/gitlab-org/build/cng/gitaly:latest
|
||||
# Disable the hooks so we don't have to stub the GitLab API
|
||||
command: ["/usr/bin/env", "GITALY_TESTING_NO_GIT_HOOKS=1", "/scripts/process-wrapper"]
|
||||
alias: gitaly
|
||||
variables:
|
||||
GITALY_ADDRESS: "tcp://gitaly:8075"
|
||||
script:
|
||||
- go version
|
||||
- apt-get update && apt-get -y install libimage-exiftool-perl
|
||||
- make test
|
||||
|
||||
test using go 1.13:
|
||||
extends: .test
|
||||
image: golang:1.13
|
||||
|
||||
test using go 1.14:
|
||||
extends: .test
|
||||
image: golang:1.14
|
||||
|
||||
test:release:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
script:
|
||||
- git describe --exact-match
|
||||
|
||||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
|
||||
gosec-sast:
|
||||
before_script:
|
||||
- apk add make
|
||||
- make install
|
||||
rules: *workflow_rules
|
||||
|
||||
gemnasium-dependency_scanning:
|
||||
rules: *workflow_rules
|
||||
|
||||
secret_detection:
|
||||
rules: *workflow_rules
|
||||
|
||||
code_navigation:
|
||||
image: golang:latest
|
||||
allow_failure: true
|
||||
script:
|
||||
- go get github.com/sourcegraph/lsif-go/cmd/lsif-go
|
||||
- lsif-go
|
||||
artifacts:
|
||||
reports:
|
||||
lsif: dump.lsif
|
|
@ -0,0 +1 @@
|
|||
* @jacobvosmaer-gitlab @nick.thomas @nolith @patrickbajao
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
## Contributing
|
||||
|
||||
Thank you for your interest in contributing to this GitLab project! We welcome
|
||||
all contributions. By participating in this project, you agree to abide by the
|
||||
[code of conduct](#code-of-conduct).
|
||||
|
||||
## Contributor license agreement
|
||||
|
||||
By submitting code as an individual you agree to the [individual contributor
|
||||
license agreement][individual-agreement].
|
||||
|
||||
By submitting code as an entity you agree to the [corporate contributor license
|
||||
agreement][corporate-agreement].
|
||||
|
||||
## Code of conduct
|
||||
|
||||
As contributors and maintainers of this project, we pledge to respect all people
|
||||
who contribute through reporting issues, posting feature requests, updating
|
||||
documentation, submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free
|
||||
experience for everyone, regardless of level of experience, gender, gender
|
||||
identity and expression, sexual orientation, disability, personal appearance,
|
||||
body size, race, ethnicity, age, or religion.
|
||||
|
||||
Examples of unacceptable behavior by participants include the use of sexual
|
||||
language or imagery, derogatory comments or personal attacks, trolling, public
|
||||
or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct. Project maintainers who do not follow the
|
||||
Code of Conduct may be removed from the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior can be
|
||||
reported by emailing contact@gitlab.com.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
|
||||
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
|
||||
|
||||
[contributor-covenant]: http://contributor-covenant.org
|
||||
[individual-agreement]: https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html
|
||||
[corporate-agreement]: https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 GitLab B.V.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,177 @@
|
|||
PREFIX=/usr/local
|
||||
PKG := gitlab.com/gitlab-org/gitlab-workhorse
|
||||
BUILD_DIR ?= $(CURDIR)
|
||||
TARGET_DIR ?= $(BUILD_DIR)/_build
|
||||
TARGET_SETUP := $(TARGET_DIR)/.ok
|
||||
BIN_BUILD_DIR := $(TARGET_DIR)/bin
|
||||
COVERAGE_DIR := $(TARGET_DIR)/cover
|
||||
VERSION_STRING := $(shell git describe)
|
||||
ifeq ($(strip $(VERSION_STRING)),)
|
||||
VERSION_STRING := v$(shell cat VERSION)
|
||||
endif
|
||||
BUILD_TIME := $(shell date -u +%Y%m%d.%H%M%S)
|
||||
GOBUILD := go build -ldflags "-X main.Version=$(VERSION_STRING) -X main.BuildTime=$(BUILD_TIME)"
|
||||
EXE_ALL := gitlab-resize-image gitlab-zip-cat gitlab-zip-metadata gitlab-workhorse
|
||||
INSTALL := install
|
||||
BUILD_TAGS := tracer_static tracer_static_jaeger continuous_profiler_stackdriver
|
||||
|
||||
MINIMUM_SUPPORTED_GO_VERSION := 1.11
|
||||
|
||||
export GOBIN := $(TARGET_DIR)/bin
|
||||
export PATH := $(GOBIN):$(PATH)
|
||||
export GOPROXY ?= https://proxy.golang.org
|
||||
export GO111MODULE=on
|
||||
|
||||
LOCAL_GO_FILES = $(shell find . -type f -name '*.go' | grep -v -e /_ -e /testdata/ -e '^\./\.')
|
||||
|
||||
define message
|
||||
@echo "### $(1)"
|
||||
endef
|
||||
|
||||
|
||||
.NOTPARALLEL:
|
||||
|
||||
.PHONY: all
|
||||
all: clean-build $(EXE_ALL)
|
||||
|
||||
$(TARGET_SETUP):
|
||||
$(call message,"Setting up target directory")
|
||||
rm -rf "$(TARGET_DIR)"
|
||||
mkdir -p "$(TARGET_DIR)"
|
||||
touch "$(TARGET_SETUP)"
|
||||
|
||||
gitlab-resize-image: $(TARGET_SETUP) $(shell find cmd/gitlab-resize-image/ -name '*.go')
|
||||
$(call message,Building $@)
|
||||
$(GOBUILD) -tags "$(BUILD_TAGS)" -o $(BUILD_DIR)/$@ $(PKG)/cmd/$@
|
||||
|
||||
gitlab-zip-cat: $(TARGET_SETUP) $(shell find cmd/gitlab-zip-cat/ -name '*.go')
|
||||
$(call message,Building $@)
|
||||
$(GOBUILD) -tags "$(BUILD_TAGS)" -o $(BUILD_DIR)/$@ $(PKG)/cmd/$@
|
||||
|
||||
gitlab-zip-metadata: $(TARGET_SETUP) $(shell find cmd/gitlab-zip-metadata/ -name '*.go')
|
||||
$(call message,Building $@)
|
||||
$(GOBUILD) -tags "$(BUILD_TAGS)" -o $(BUILD_DIR)/$@ $(PKG)/cmd/$@
|
||||
|
||||
gitlab-workhorse: $(TARGET_SETUP) $(shell find . -name '*.go' | grep -v '^\./_')
|
||||
$(call message,Building $@)
|
||||
$(GOBUILD) -tags "$(BUILD_TAGS)" -o $(BUILD_DIR)/$@ $(PKG)
|
||||
|
||||
.PHONY: install
|
||||
install: $(EXE_ALL)
|
||||
$(call message,$@)
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/bin/
|
||||
cd $(BUILD_DIR) && $(INSTALL) $(EXE_ALL) $(DESTDIR)$(PREFIX)/bin/
|
||||
|
||||
.PHONY: test
|
||||
test: $(TARGET_SETUP) prepare-tests
|
||||
$(call message,$@)
|
||||
@go test -tags "$(BUILD_TAGS)" ./...
|
||||
@echo SUCCESS
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: $(TARGET_SETUP) prepare-tests
|
||||
$(call message,$@)
|
||||
@go test -tags "$(BUILD_TAGS)" -cover -coverprofile=test.coverage ./...
|
||||
go tool cover -html=test.coverage -o coverage.html
|
||||
rm -f test.coverage
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-workhorse clean-build
|
||||
$(call message,$@)
|
||||
rm -rf testdata/data testdata/scratch
|
||||
|
||||
.PHONY: clean-workhorse
|
||||
clean-workhorse:
|
||||
$(call message,$@)
|
||||
rm -f $(EXE_ALL)
|
||||
|
||||
.PHONY: check-version
|
||||
check-version:
|
||||
@test -n "$(VERSION)" || (echo "VERSION not set." ; exit 1)
|
||||
|
||||
.PHONY: tag
|
||||
tag: check-version
|
||||
$(call message,$@)
|
||||
sh _support/tag.sh "$(VERSION)"
|
||||
|
||||
.PHONY: signed_tag
|
||||
signed_tag: check-version
|
||||
$(call message,$@)
|
||||
TAG_OPTS=-s sh _support/tag.sh "$(VERSION)"
|
||||
|
||||
.PHONY: clean-build
|
||||
clean-build:
|
||||
$(call message,$@)
|
||||
rm -rf $(TARGET_DIR)
|
||||
|
||||
.PHONY: prepare-tests
|
||||
prepare-tests: testdata/data/group/test.git $(EXE_ALL)
|
||||
prepare-tests: testdata/scratch
|
||||
|
||||
testdata/data/group/test.git:
|
||||
$(call message,$@)
|
||||
git clone --quiet --bare https://gitlab.com/gitlab-org/gitlab-test.git $@
|
||||
|
||||
testdata/scratch:
|
||||
mkdir -p testdata/scratch
|
||||
|
||||
.PHONY: verify
|
||||
verify: lint vet detect-context detect-assert check-formatting staticcheck deps-check
|
||||
|
||||
.PHONY: lint
|
||||
lint: $(TARGET_SETUP)
|
||||
$(call message,Verify: $@)
|
||||
go install golang.org/x/lint/golint
|
||||
@_support/lint.sh ./...
|
||||
|
||||
.PHONY: vet
|
||||
vet: $(TARGET_SETUP)
|
||||
$(call message,Verify: $@)
|
||||
@go vet ./...
|
||||
|
||||
.PHONY: detect-context
|
||||
detect-context: $(TARGET_SETUP)
|
||||
$(call message,Verify: $@)
|
||||
_support/detect-context.sh
|
||||
|
||||
.PHONY: detect-assert
|
||||
detect-assert:
|
||||
$(call message,Verify: $@)
|
||||
_support/detect-assert.sh
|
||||
|
||||
.PHONY: check-formatting
|
||||
check-formatting: $(TARGET_SETUP) install-goimports
|
||||
$(call message,Verify: $@)
|
||||
@_support/validate-formatting.sh $(LOCAL_GO_FILES)
|
||||
|
||||
# Megacheck will tailor some responses given a minimum Go version, so pass that through the CLI
|
||||
# Additionally, megacheck will not return failure exit codes unless explicitly told to via the
|
||||
# `-simple.exit-non-zero` `-unused.exit-non-zero` and `-staticcheck.exit-non-zero` flags
|
||||
.PHONY: staticcheck
|
||||
staticcheck: $(TARGET_SETUP)
|
||||
$(call message,Verify: $@)
|
||||
go install honnef.co/go/tools/cmd/staticcheck
|
||||
@ $(GOBIN)/staticcheck -go $(MINIMUM_SUPPORTED_GO_VERSION) ./...
|
||||
|
||||
# In addition to fixing imports, goimports also formats your code in the same style as gofmt
|
||||
# so it can be used as a replacement.
|
||||
.PHONY: fmt
|
||||
fmt: $(TARGET_SETUP) install-goimports
|
||||
$(call message,$@)
|
||||
@goimports -w -local $(PKG) -l $(LOCAL_GO_FILES)
|
||||
|
||||
.PHONY: goimports
|
||||
install-goimports: $(TARGET_SETUP)
|
||||
$(call message,$@)
|
||||
go install golang.org/x/tools/cmd/goimports
|
||||
|
||||
.PHONY: deps-check
|
||||
deps-check:
|
||||
go mod tidy
|
||||
@if git diff --quiet --exit-code -- go.mod go.sum; then \
|
||||
echo "go.mod and go.sum are ok"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "go.mod and go.sum are modified, please commit them";\
|
||||
exit 1; \
|
||||
fi;
|
|
@ -0,0 +1,152 @@
|
|||
# GitLab-Workhorse development process
|
||||
|
||||
## Maintainers
|
||||
|
||||
GitLab-Workhorse has the following maintainers:
|
||||
|
||||
- Nick Thomas `@nick.thomas`
|
||||
- Jacob Vosmaer `@jacobvosmaer-gitlab`
|
||||
- Alessio Caiazza `@nolith`
|
||||
|
||||
This list is defined at https://about.gitlab.com/team/.
|
||||
|
||||
## Changelog
|
||||
|
||||
GitLab-Workhorse keeps a changelog which is generated when a new release
|
||||
is created. The changelog is generated from entries that are included on each
|
||||
merge request. To generate an entry on your branch run:
|
||||
`_support/changelog "Change descriptions"`.
|
||||
|
||||
After the merge request is created, the ID of the merge request needs to be set
|
||||
in the generated file. If you already know the merge request ID, run:
|
||||
`_support/changelog -m <ID> "Change descriptions"`.
|
||||
|
||||
Any new merge request must contain either a new entry or a justification in the
|
||||
merge request description why no changelog entry is needed.
|
||||
|
||||
## Merging and reviewing contributions
|
||||
|
||||
Contributions must be reviewed by at least one Workhorse maintainer.
|
||||
The final merge must be performed by a maintainer.
|
||||
|
||||
## Releases
|
||||
|
||||
New versions of Workhorse can be released by one of the Workhorse
|
||||
maintainers. The release process is:
|
||||
|
||||
- pick a release branch. For x.y.0, use `master`. For all other
|
||||
versions (x.y.1, x.y.2 etc.) , use `x-y-stable`. Also see [below](#versioning)
|
||||
- run `make tag VERSION=x.y.z"` or `make signed_tag VERSION=x.y.z` on the release branch. This will
|
||||
compile the changelog, bump the VERSION file, and make a tag matching it.
|
||||
- push the branch and the tag to gitlab.com
|
||||
- the new version will only be deployed to `gitlab.com` if [`GITLAB_WORKHORSE_VERSION`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/GITLAB_WORKHORSE_VERSION) is updated accordingly;
|
||||
if applicable, please remind the person who originally asked for a new release to make this change
|
||||
(the MR should include a link back to the [version tag](https://gitlab.com/gitlab-org/gitlab-workhorse/-/tags) and a copy of the changelog)
|
||||
|
||||
## Security releases
|
||||
|
||||
Workhorse is included in the packages we create for GitLab, and each version of
|
||||
GitLab specifies the version of Workhorse it uses in the `GITLAB_WORKHORSE_VERSION`
|
||||
file, so security fixes in Workhorse are tightly coupled to the [general security release](https://about.gitlab.com/handbook/engineering/workflow/#security-issues)
|
||||
workflow, with some elaborations to account for the changes happening across two
|
||||
repositories. In particular, the Workhorse maintainer takes responsibility for
|
||||
creating new patch versions of Workhorse that can be used in the security
|
||||
release.
|
||||
|
||||
As security fixes are backported three releases in addition to master, and
|
||||
changes need to happen across two repositories, up to eight merge requests, and
|
||||
four Workhorse releases, can be required to fix a security issue in Workhorse.
|
||||
This is a lot of overhead, so in general, it is better to fix security issues
|
||||
without changing Workhorse. Where changes **are** necessary, this section
|
||||
documents the necessary steps.
|
||||
|
||||
If you're working on a security fix in Workhorse, you need two sets of merge
|
||||
requests:
|
||||
|
||||
* The fix itself, in the `gitlab-org/security/gitlab-workhorse` repository
|
||||
* A merge request to change the version of workhorse included in the GitLab
|
||||
security release, in the `gitlab-org/security/gitlab` repository.
|
||||
|
||||
If the Workhorse maintainer isn't also a GitLab maintainer, reviews will need to
|
||||
be split across several people. If changes to GitLab **code** are required in
|
||||
addition to the change of Workhorse version, they both happen in the same merge
|
||||
request.
|
||||
|
||||
Start by creating a single merge request targeting `master` in Workhorse. Ensure
|
||||
you include a changelog! If code changes are needed in GitLab as well, create a
|
||||
GitLab merge request targeting `master` at this point, but don't worry about the
|
||||
`GITLAB_WORKHORSE_VERSION` file yet.
|
||||
|
||||
Once the changes have passed review, the Workhorse maintainer will determine the
|
||||
new versions of Workhorse that will be needed, and communicate that to the
|
||||
author. To do this, examine the `GITLAB_WORKHORSE_VERSION` file on each GitLab
|
||||
stable branch; for instance, if the security release consisted of GitLab
|
||||
versions `12.10.1`, `12.9.2`, `12.8.3`, and `12.7.4`, we would see the following:
|
||||
|
||||
```
|
||||
gitlab$ git fetch security master 12-10-stable-ee 12-9-stable-ee 12-8-stable-ee 12-7-stable-ee`
|
||||
gitlab$ git show refs/remotes/security/master:GITLAB_WORKHORSE_VERSION
|
||||
8.30.1
|
||||
gitlab$ git show refs/remotes/security/12-10-stable-ee:GITLAB_WORKHORSE_VERSION
|
||||
8.30.1
|
||||
gitlab$ git show refs/remotes/security/12-9-stable-ee:GITLAB_WORKHORSE_VERSION
|
||||
8.25.2
|
||||
gitlab$ git show refs/remotes/security/12-8-stable-ee:GITLAB_WORKHORSE_VERSION
|
||||
8.21.2
|
||||
gitlab$ git show refs/remotes/security/12-7-stable-ee:GITLAB_WORKHORSE_VERSION
|
||||
8.21.2
|
||||
```
|
||||
|
||||
In this example, there are three distinct Workhorse stable branches to be
|
||||
concerned with, plus Workhorse master: `8-30-stable`, `8-25-stable`, and
|
||||
`8-21-stable`, and we can predict that we are going to need to create Workhorse
|
||||
releases `8.30.2`, `8.25.3`, and `8.21.3`.
|
||||
|
||||
The author needs to create a merge request targeting each Workhorse stable
|
||||
branch, and verify that the fix works once backported. They also need to create
|
||||
(or update, if they already exist) GitLab merge requests, setting the
|
||||
`GITLAB_WORKHORSE_VERSION` file to the predicted workhorse version, and assign
|
||||
all the MRs back to the appropriate maintainer(s). The pipeline for the GitLab
|
||||
MRs will fail until the Workhorse releases have been tagged; you can use the
|
||||
`=workhorse_branch_name` syntax in the `GITLAB_WORKHORSE_VERSION` file to verify
|
||||
that the MRs interact as expected, if necessary.
|
||||
|
||||
Once all involved maintainers are happy with the overall change, the Workhorse
|
||||
maintainer will merge each of the Workhorse MRs and generate new Workhorse
|
||||
releases from the stable branches. The tags will be present on the `security`
|
||||
mirror and `dev.gitlab.org` **only** at this point.
|
||||
|
||||
Once the Workhorse tags exist, the GitLab maintainer ensures that all the GitLab
|
||||
MRs are green and assigns those MRs on to the release bot.
|
||||
|
||||
The release managers merge the GitLab MRs, tag GitLab releases that reference
|
||||
the new Workhorse tags, and release them in the usual way.
|
||||
|
||||
Once the security release is done, the Workhorse maintainer is responsible for
|
||||
syncing the changes to the `gitlab-org/gitlab-workhorse` repository. Push the
|
||||
changes to `master`, the new tags, and all the changes to the stable branches.
|
||||
|
||||
This process is quite involved, very manual, and extremely error-prone; work is
|
||||
ongoing on automating it.
|
||||
|
||||
## Versioning
|
||||
|
||||
Workhorse uses a variation of SemVer. We don't use "normal" SemVer
|
||||
because we have to be able to integrate into GitLab stable branches.
|
||||
|
||||
A version has the format MAJOR.MINOR.PATCH.
|
||||
|
||||
- Major and minor releases are tagged on the `master` branch
|
||||
- If the change is backwards compatible, increment the MINOR counter
|
||||
- If the change breaks compatibility, increment MAJOR and set MINOR to `0`
|
||||
- Patch release tags must be made on stable branches
|
||||
- Only make a patch release when targeting a GitLab stable branch
|
||||
|
||||
This means that tags that end in `.0` (e.g. `8.5.0`) must always be on
|
||||
the master branch, and tags that end in anthing other than `.0` (e.g.
|
||||
`8.5.2`) must always be on a stable branch.
|
||||
|
||||
> The reason we do this is that SemVer suggests something like a
|
||||
> refactoring constitutes a "patch release", while the GitLab stable
|
||||
> branch quality standards do not allow for back-porting refactorings
|
||||
> into a stable branch.
|
|
@ -0,0 +1,27 @@
|
|||
# GitLab Workhorse
|
||||
|
||||
GitLab Workhorse is a smart reverse proxy for GitLab. It handles
|
||||
"large" HTTP requests such as file downloads, file uploads, Git
|
||||
push/pull and Git archive downloads.
|
||||
|
||||
Workhorse itself is not a feature, but there are [several features in
|
||||
GitLab](doc/architecture/gitlab_features.md) that would not work efficiently without Workhorse.
|
||||
|
||||
## Documentation
|
||||
|
||||
Workhorse documentation is available in the [`doc` folder of this repository](doc/).
|
||||
|
||||
* Architectural overview
|
||||
* [GitLab features that rely on Workhorse](doc/architecture/gitlab_features.md)
|
||||
* [Websocket channel support](doc/architecture/channel.md)
|
||||
* Operating Workhorse
|
||||
* [Source installation](doc/operations/install.md)
|
||||
* [Workhorse configuration](doc/operations/configuration.md)
|
||||
* [Contributing](CONTRIBUTING.md)
|
||||
* [Adding new features](doc/development/new_features.md)
|
||||
* [Testing your code](doc/development/tests.md)
|
||||
|
||||
## License
|
||||
|
||||
This code is distributed under the MIT license, see the [LICENSE](LICENSE) file.
|
||||
|
|
@ -0,0 +1 @@
|
|||
8.57.0
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env ruby
|
||||
#
|
||||
# Generate a changelog entry file in the correct location.
|
||||
#
|
||||
# Automatically stages the file and amends the previous commit if the `--amend`
|
||||
# argument is used.
|
||||
#
|
||||
# Stolen from gitlab-org/gitaly, lifted from gitlab-org/gitlab-ce
|
||||
|
||||
require 'optparse'
|
||||
require 'yaml'
|
||||
|
||||
Options = Struct.new(
|
||||
:amend,
|
||||
:author,
|
||||
:dry_run,
|
||||
:force,
|
||||
:merge_request,
|
||||
:title,
|
||||
:type
|
||||
)
|
||||
INVALID_TYPE = -1
|
||||
|
||||
class ChangelogOptionParser
|
||||
Type = Struct.new(:name, :description)
|
||||
TYPES = [
|
||||
Type.new('added', 'New feature'),
|
||||
Type.new('fixed', 'Bug fix'),
|
||||
Type.new('changed', 'Feature change'),
|
||||
Type.new('deprecated', 'New deprecation'),
|
||||
Type.new('removed', 'Feature removal'),
|
||||
Type.new('security', 'Security fix'),
|
||||
Type.new('performance', 'Performance improvement'),
|
||||
Type.new('other', 'Other')
|
||||
].freeze
|
||||
TYPES_OFFSET = 1
|
||||
|
||||
class << self
|
||||
def parse(argv)
|
||||
options = Options.new
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
|
||||
|
||||
# Note: We do not provide a shorthand for this in order to match the `git
|
||||
# commit` interface
|
||||
opts.on('--amend', 'Amend the previous commit') do |value|
|
||||
options.amend = value
|
||||
end
|
||||
|
||||
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
|
||||
options.force = value
|
||||
end
|
||||
|
||||
opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
|
||||
options.merge_request = value
|
||||
end
|
||||
|
||||
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
|
||||
options.dry_run = value
|
||||
end
|
||||
|
||||
opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
|
||||
options.author = git_user_name if value
|
||||
end
|
||||
|
||||
opts.on('-t', '--type [string]', String, "The category of the change, valid options are: #{TYPES.map(&:name).join(', ')}") do |value|
|
||||
options.type = parse_type(value)
|
||||
end
|
||||
|
||||
opts.on('-h', '--help', 'Print help message') do
|
||||
$stdout.puts opts
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
parser.parse!(argv)
|
||||
|
||||
# Title is everything that remains, but let's clean it up a bit
|
||||
options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def read_type
|
||||
read_type_message
|
||||
|
||||
type = TYPES[$stdin.getc.to_i - TYPES_OFFSET]
|
||||
assert_valid_type!(type)
|
||||
|
||||
type.name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_type(name)
|
||||
type_found = TYPES.find do |type|
|
||||
type.name == name
|
||||
end
|
||||
type_found ? type_found.name : INVALID_TYPE
|
||||
end
|
||||
|
||||
def read_type_message
|
||||
$stdout.puts "\n>> Please specify the index for the category of your change:"
|
||||
TYPES.each_with_index do |type, index|
|
||||
$stdout.puts "#{index + TYPES_OFFSET}. #{type.description}"
|
||||
end
|
||||
$stdout.print "\n?> "
|
||||
end
|
||||
|
||||
def assert_valid_type!(type)
|
||||
unless type
|
||||
$stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def git_user_name
|
||||
%x{git config user.name}.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ChangelogEntry
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
|
||||
assert_feature_branch!
|
||||
assert_title!
|
||||
assert_new_file!
|
||||
|
||||
# Read type from $stdin unless is already set
|
||||
options.type ||= ChangelogOptionParser.read_type
|
||||
assert_valid_type!
|
||||
|
||||
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
|
||||
$stdout.puts contents
|
||||
|
||||
unless options.dry_run
|
||||
write
|
||||
amend_commit if options.amend
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contents
|
||||
yaml_content = YAML.dump(
|
||||
'title' => title,
|
||||
'merge_request' => options.merge_request,
|
||||
'author' => options.author,
|
||||
'type' => options.type
|
||||
)
|
||||
remove_trailing_whitespace(yaml_content)
|
||||
end
|
||||
|
||||
def write
|
||||
File.write(file_path, contents)
|
||||
end
|
||||
|
||||
def amend_commit
|
||||
%x{git add #{file_path}}
|
||||
exec("git commit --amend")
|
||||
end
|
||||
|
||||
def fail_with(message)
|
||||
$stderr.puts "\e[31merror\e[0m #{message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
def assert_feature_branch!
|
||||
return unless branch_name == 'master'
|
||||
|
||||
fail_with "Create a branch first!"
|
||||
end
|
||||
|
||||
def assert_new_file!
|
||||
return unless File.exist?(file_path)
|
||||
return if options.force
|
||||
|
||||
fail_with "#{file_path} already exists! Use `--force` to overwrite."
|
||||
end
|
||||
|
||||
def assert_title!
|
||||
return if options.title.length > 0 || options.amend
|
||||
|
||||
fail_with "Provide a title for the changelog entry or use `--amend`" \
|
||||
" to use the title from the previous commit."
|
||||
end
|
||||
|
||||
def assert_valid_type!
|
||||
return unless options.type && options.type == INVALID_TYPE
|
||||
|
||||
fail_with 'Invalid category given!'
|
||||
end
|
||||
|
||||
def title
|
||||
if options.title.empty?
|
||||
last_commit_subject
|
||||
else
|
||||
options.title
|
||||
end
|
||||
end
|
||||
|
||||
def last_commit_subject
|
||||
%x{git log --format="%s" -1}.strip
|
||||
end
|
||||
|
||||
def file_path
|
||||
File.join(
|
||||
unreleased_path,
|
||||
branch_name.gsub(/[^\w-]/, '-') << '.yml'
|
||||
)
|
||||
end
|
||||
|
||||
def unreleased_path
|
||||
path = File.join('changelogs', 'unreleased')
|
||||
path = File.join('ee', path) if ee?
|
||||
|
||||
path
|
||||
end
|
||||
|
||||
def ee?
|
||||
@ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
|
||||
end
|
||||
|
||||
def branch_name
|
||||
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip
|
||||
end
|
||||
|
||||
def remove_trailing_whitespace(yaml_content)
|
||||
yaml_content.gsub(/ +$/, '')
|
||||
end
|
||||
end
|
||||
|
||||
if $0 == __FILE__
|
||||
options = ChangelogOptionParser.parse(ARGV)
|
||||
ChangelogEntry.new(options)
|
||||
end
|
||||
|
||||
# vim: ft=ruby
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# we skip the changelog check if the merge requet title ends with "NO CHANGELOG"
|
||||
if echo "$CI_MERGE_REQUEST_TITLE" | grep -q ' NO CHANGELOG$'; then
|
||||
echo "Changelog not needed"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
target=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-master}
|
||||
|
||||
if git diff --name-only "origin/$target" | grep -q '^changelogs/' ; then
|
||||
echo "Changelog included"
|
||||
else
|
||||
echo "Please add a changelog running '_support/changelog'"
|
||||
echo "or disable this check adding 'NO CHANGELOG' at the end of the merge request title"
|
||||
echo "/title $CI_MERGE_REQUEST_TITLE NO CHANGELOG"
|
||||
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
git grep 'testify/assert"' | \
|
||||
grep -e '^[^:]*\.go' | \
|
||||
awk '{
|
||||
print "error: please use testify/require instead of testify/assert"
|
||||
print
|
||||
exit 1
|
||||
}'
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
git grep 'context.\(Background\|TODO\)' | \
|
||||
grep -v -e '^[^:]*_test\.go:' -v -e "lint:allow context.Background" -e '^vendor/' -e '^_support/' -e '^cmd/[^:]*/main.go' | \
|
||||
grep -e '^[^:]*\.go' | \
|
||||
awk '{
|
||||
print "Found disallowed use of context.Background or TODO"
|
||||
print
|
||||
exit 1
|
||||
}'
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s /path/to/test-repo.git\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"RepoPath":"%s","ArchivePath":"%s"}`, os.Args[1], r.URL.Path)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", nil))
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env ruby
|
||||
# Generates the changelog from the yaml entries in changelogs/unreleased
|
||||
#
|
||||
# Lifted form gitlab-org/gitaly
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
|
||||
class ChangelogEntry
|
||||
attr_reader :title, :merge_request, :type, :author
|
||||
|
||||
def initialize(file_path)
|
||||
yaml = YAML.safe_load(File.read(file_path))
|
||||
|
||||
@title = yaml['title']
|
||||
@merge_request = yaml['merge_request']
|
||||
@type = yaml['type']
|
||||
@author = yaml['author']
|
||||
end
|
||||
|
||||
def to_s
|
||||
str = ""
|
||||
str << "- #{title}\n"
|
||||
str << " https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/#{merge_request}\n"
|
||||
str << " Contributed by #{author}\n" if author
|
||||
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
ROOT_DIR = File.expand_path('../..', __FILE__)
|
||||
UNRELEASED_ENTRIES = File.join(ROOT_DIR, 'changelogs', 'unreleased')
|
||||
CHANGELOG_FILE = File.join(ROOT_DIR, 'CHANGELOG')
|
||||
|
||||
def main(version)
|
||||
entries = []
|
||||
Dir["#{UNRELEASED_ENTRIES}/*.yml"].each do |yml|
|
||||
entries << ChangelogEntry.new(yml)
|
||||
FileUtils.rm(yml)
|
||||
end
|
||||
|
||||
sections = []
|
||||
types = entries.map(&:type).uniq.sort
|
||||
types.each do |type|
|
||||
text = ''
|
||||
text << "### #{type.capitalize}\n"
|
||||
|
||||
entries.each do |e|
|
||||
next unless e.type == type
|
||||
|
||||
text << e.to_s
|
||||
end
|
||||
|
||||
sections << text
|
||||
end
|
||||
|
||||
sections << '- No changes.' if sections.empty?
|
||||
|
||||
new_version_entry = ["## v#{version}\n\n", sections.join("\n"), "\n"].join
|
||||
|
||||
current_changelog = File.read(CHANGELOG_FILE).lines
|
||||
header = current_changelog.shift(2)
|
||||
|
||||
new_changelog = [header, new_version_entry, current_changelog.join]
|
||||
|
||||
File.write(CHANGELOG_FILE, new_changelog.join)
|
||||
end
|
||||
|
||||
unless ARGV.count == 1
|
||||
warn "Usage: #{$0} VERSION"
|
||||
warn "Specify version as x.y.z"
|
||||
abort
|
||||
end
|
||||
|
||||
main(ARGV.first)
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Unfortunately, workhorse fails many lint checks which we currently ignore
|
||||
LINT_RESULT=$(golint "$@"|grep -Ev 'should have|should be|use ALL_CAPS in Go names')
|
||||
|
||||
if [ -n "${LINT_RESULT}" ]; then
|
||||
echo >&2 "Formatting or imports need fixing: 'make fmt'"
|
||||
echo ">>${LINT_RESULT}<<"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
set -e
|
||||
|
||||
main() {
|
||||
version=$1
|
||||
set_version
|
||||
|
||||
changelog
|
||||
|
||||
git commit VERSION -m "Update VERSION to $version"
|
||||
|
||||
tag_name="v${version}"
|
||||
git tag $TAG_OPTS -m "Version ${version}" -a ${tag_name}
|
||||
git show ${tag_name}
|
||||
cat <<'EOF'
|
||||
|
||||
Remember to now push your tag, either to gitlab.com (for a
|
||||
normal release) or dev.gitlab.org (for a security release).
|
||||
EOF
|
||||
}
|
||||
|
||||
set_version() {
|
||||
if ! echo "${version}" | grep -q '^[0-9]\+\.[0-9]\+\.[0-9]\+$' ; then
|
||||
echo "Invalid VERSION: ${version}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git tag --list | grep -q "^v${version}$" ; then
|
||||
echo "Tag already exists for ${version}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$version" > VERSION
|
||||
}
|
||||
|
||||
changelog() {
|
||||
_support/generate_changelog "$version"
|
||||
|
||||
git commit CHANGELOG changelogs/unreleased --file - <<EOF
|
||||
Update CHANGELOG for ${version}
|
||||
|
||||
[ci skip]
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
IMPORT_RESULT=$(goimports -e -local "gitlab.com/gitlab-org/gitlab-workhorse" -l "$@")
|
||||
|
||||
if [ -n "${IMPORT_RESULT}" ]; then
|
||||
echo >&2 "Formatting or imports need fixing: 'make fmt'"
|
||||
echo "${IMPORT_RESULT}"
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,131 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/correlation"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper"
|
||||
)
|
||||
|
||||
func okHandler(w http.ResponseWriter, _ *http.Request, _ *api.Response) {
|
||||
w.WriteHeader(201)
|
||||
fmt.Fprint(w, "{\"status\":\"ok\"}")
|
||||
}
|
||||
|
||||
func runPreAuthorizeHandler(t *testing.T, ts *httptest.Server, suffix string, url *regexp.Regexp, apiResponse interface{}, returnCode, expectedCode int) *httptest.ResponseRecorder {
|
||||
if ts == nil {
|
||||
ts = testAuthServer(t, url, nil, returnCode, apiResponse)
|
||||
defer ts.Close()
|
||||
}
|
||||
|
||||
// Create http request
|
||||
ctx := correlation.ContextWithCorrelation(context.Background(), "12345678")
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, "GET", "/address", nil)
|
||||
require.NoError(t, err)
|
||||
parsedURL := helper.URLMustParse(ts.URL)
|
||||
testhelper.ConfigureSecret()
|
||||
a := api.NewAPI(parsedURL, "123", roundtripper.NewTestBackendRoundTripper(parsedURL))
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
a.PreAuthorizeHandler(okHandler, suffix).ServeHTTP(response, httpRequest)
|
||||
require.Equal(t, expectedCode, response.Code)
|
||||
return response
|
||||
}
|
||||
|
||||
func TestPreAuthorizeHappyPath(t *testing.T) {
|
||||
runPreAuthorizeHandler(
|
||||
t, nil, "/authorize",
|
||||
regexp.MustCompile(`/authorize\z`),
|
||||
&api.Response{},
|
||||
200, 201)
|
||||
}
|
||||
|
||||
func TestPreAuthorizeSuffix(t *testing.T) {
|
||||
runPreAuthorizeHandler(
|
||||
t, nil, "/different-authorize",
|
||||
regexp.MustCompile(`/authorize\z`),
|
||||
&api.Response{},
|
||||
200, 404)
|
||||
}
|
||||
|
||||
func TestPreAuthorizeJsonFailure(t *testing.T) {
|
||||
runPreAuthorizeHandler(
|
||||
t, nil, "/authorize",
|
||||
regexp.MustCompile(`/authorize\z`),
|
||||
"not-json",
|
||||
200, 500)
|
||||
}
|
||||
|
||||
func TestPreAuthorizeContentTypeFailure(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := w.Write([]byte(`{"hello":"world"}`))
|
||||
require.NoError(t, err, "write auth response")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
runPreAuthorizeHandler(
|
||||
t, ts, "/authorize",
|
||||
regexp.MustCompile(`/authorize\z`),
|
||||
"",
|
||||
200, 200)
|
||||
}
|
||||
|
||||
func TestPreAuthorizeRedirect(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
runPreAuthorizeHandler(t, ts, "/willredirect",
|
||||
regexp.MustCompile(`/willredirect\z`),
|
||||
"",
|
||||
301, 301)
|
||||
}
|
||||
|
||||
func TestPreAuthorizeJWT(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := jwt.Parse(r.Header.Get(secret.RequestHeader), func(token *jwt.Token) (interface{}, error) {
|
||||
// Don't forget to validate the alg is what you expect:
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
testhelper.ConfigureSecret()
|
||||
secretBytes, err := secret.Bytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read secret from file: %v", err)
|
||||
}
|
||||
|
||||
return secretBytes, nil
|
||||
})
|
||||
require.NoError(t, err, "decode token")
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
require.True(t, ok, "claims cast")
|
||||
require.True(t, token.Valid, "JWT token valid")
|
||||
require.Equal(t, "gitlab-workhorse", claims["iss"], "JWT token issuer")
|
||||
|
||||
w.Header().Set("Content-Type", api.ResponseContentType)
|
||||
_, err = w.Write([]byte(`{"hello":"world"}`))
|
||||
require.NoError(t, err, "write auth response")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
runPreAuthorizeHandler(
|
||||
t, ts, "/authorize",
|
||||
regexp.MustCompile(`/authorize\z`),
|
||||
"",
|
||||
200, 201)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func parseAuthBackend(authBackend string) (*url.URL, error) {
|
||||
backendURL, err := url.Parse(authBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if backendURL.Host == "" {
|
||||
backendURL, err = url.Parse("http://" + authBackend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if backendURL.Scheme != "http" {
|
||||
return nil, fmt.Errorf("invalid scheme, only 'http' is allowed: %q", authBackend)
|
||||
}
|
||||
|
||||
if backendURL.Host == "" {
|
||||
return nil, fmt.Errorf("missing host in %q", authBackend)
|
||||
}
|
||||
|
||||
return backendURL, nil
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseAuthBackendFailure(t *testing.T) {
|
||||
failures := []string{
|
||||
"",
|
||||
"ftp://localhost",
|
||||
"https://example.com",
|
||||
}
|
||||
|
||||
for _, example := range failures {
|
||||
t.Run(example, func(t *testing.T) {
|
||||
_, err := parseAuthBackend(example)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuthBackend(t *testing.T) {
|
||||
successes := []struct{ input, host, scheme string }{
|
||||
{"http://localhost:8080", "localhost:8080", "http"},
|
||||
{"localhost:3000", "localhost:3000", "http"},
|
||||
{"http://localhost", "localhost", "http"},
|
||||
{"localhost", "localhost", "http"},
|
||||
}
|
||||
|
||||
for _, example := range successes {
|
||||
t.Run(example.input, func(t *testing.T) {
|
||||
result, err := parseAuthBackend(example.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, example.host, result.Host, "host")
|
||||
require.Equal(t, example.scheme, result.Scheme, "scheme")
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
|
||||
)
|
||||
|
||||
const cablePath = "/-/cable"
|
||||
|
||||
func TestSingleBackend(t *testing.T) {
|
||||
cableServerConns, cableBackendServer := startCableServer()
|
||||
defer cableBackendServer.Close()
|
||||
|
||||
config := newUpstreamWithCableConfig(cableBackendServer.URL, "")
|
||||
workhorse := startWorkhorseServerWithConfig(config)
|
||||
defer workhorse.Close()
|
||||
|
||||
cableURL := websocketURL(workhorse.URL, cablePath)
|
||||
|
||||
client, _, err := dialWebsocket(cableURL, nil)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
server := (<-cableServerConns).conn
|
||||
defer server.Close()
|
||||
|
||||
require.NoError(t, say(client, "hello"))
|
||||
requireReadMessage(t, server, websocket.TextMessage, "hello")
|
||||
|
||||
require.NoError(t, say(server, "world"))
|
||||
requireReadMessage(t, client, websocket.TextMessage, "world")
|
||||
}
|
||||
|
||||
func TestSeparateCableBackend(t *testing.T) {
|
||||
authBackendServer := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), http.HandlerFunc(http.NotFound))
|
||||
defer authBackendServer.Close()
|
||||
|
||||
cableServerConns, cableBackendServer := startCableServer()
|
||||
defer cableBackendServer.Close()
|
||||
|
||||
config := newUpstreamWithCableConfig(authBackendServer.URL, cableBackendServer.URL)
|
||||
workhorse := startWorkhorseServerWithConfig(config)
|
||||
defer workhorse.Close()
|
||||
|
||||
cableURL := websocketURL(workhorse.URL, cablePath)
|
||||
|
||||
client, _, err := dialWebsocket(cableURL, nil)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
server := (<-cableServerConns).conn
|
||||
defer server.Close()
|
||||
|
||||
require.NoError(t, say(client, "hello"))
|
||||
requireReadMessage(t, server, websocket.TextMessage, "hello")
|
||||
|
||||
require.NoError(t, say(server, "world"))
|
||||
requireReadMessage(t, client, websocket.TextMessage, "world")
|
||||
}
|
||||
|
||||
func startCableServer() (chan connWithReq, *httptest.Server) {
|
||||
upgrader := &websocket.Upgrader{}
|
||||
|
||||
connCh := make(chan connWithReq, 1)
|
||||
server := testhelper.TestServerWithHandler(regexp.MustCompile(cablePath), webSocketHandler(upgrader, connCh))
|
||||
|
||||
return connCh, server
|
||||
}
|
||||
|
||||
func newUpstreamWithCableConfig(authBackend string, cableBackend string) *config.Config {
|
||||
var cableBackendURL *url.URL
|
||||
|
||||
if cableBackend != "" {
|
||||
cableBackendURL = helper.URLMustParse(cableBackend)
|
||||
}
|
||||
|
||||
return &config.Config{
|
||||
Version: "123",
|
||||
DocumentRoot: testDocumentRoot,
|
||||
Backend: helper.URLMustParse(authBackend),
|
||||
CableBackend: cableBackendURL,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/gitlab-org/labkit/log"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
|
||||
)
|
||||
|
||||
var (
|
||||
envTerminalPath = fmt.Sprintf("%s/-/environments/1/terminal.ws", testProject)
|
||||
jobTerminalPath = fmt.Sprintf("%s/-/jobs/1/terminal.ws", testProject)
|
||||
servicesProxyWSPath = fmt.Sprintf("%s/-/jobs/1/proxy.ws", testProject)
|
||||
)
|
||||
|
||||
type connWithReq struct {
|
||||
conn *websocket.Conn
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
func TestChannelHappyPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
channelPath string
|
||||
}{
|
||||
{"environments", envTerminalPath},
|
||||
{"jobs", jobTerminalPath},
|
||||
{"services", servicesProxyWSPath},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
serverConns, clientURL, close := wireupChannel(t, test.channelPath, nil, "channel.k8s.io")
|
||||
defer close()
|
||||
|
||||
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := (<-serverConns).conn
|
||||
defer server.Close()
|
||||
|
||||
message := "test message"
|
||||
|
||||
// channel.k8s.io: server writes to channel 1, STDOUT
|
||||
require.NoError(t, say(server, "\x01"+message))
|
||||
requireReadMessage(t, client, websocket.BinaryMessage, message)
|
||||
|
||||
require.NoError(t, say(client, message))
|
||||
|
||||
// channel.k8s.io: client writes get put on channel 0, STDIN
|
||||
requireReadMessage(t, server, websocket.BinaryMessage, "\x00"+message)
|
||||
|
||||
// Closing the client should send an EOT signal to the server's STDIN
|
||||
client.Close()
|
||||
requireReadMessage(t, server, websocket.BinaryMessage, "\x00\x04")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelBadTLS(t *testing.T) {
|
||||
_, clientURL, close := wireupChannel(t, envTerminalPath, badCA, "channel.k8s.io")
|
||||
defer close()
|
||||
|
||||
_, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
|
||||
require.Equal(t, websocket.ErrBadHandshake, err, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
func TestChannelSessionTimeout(t *testing.T) {
|
||||
serverConns, clientURL, close := wireupChannel(t, envTerminalPath, timeout, "channel.k8s.io")
|
||||
defer close()
|
||||
|
||||
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
sc := <-serverConns
|
||||
defer sc.conn.Close()
|
||||
|
||||
client.SetReadDeadline(time.Now().Add(time.Duration(2) * time.Second))
|
||||
_, _, err = client.ReadMessage()
|
||||
|
||||
require.True(t, websocket.IsCloseError(err, websocket.CloseAbnormalClosure), "Client connection was not closed, got %v", err)
|
||||
}
|
||||
|
||||
func TestChannelProxyForwardsHeadersFromUpstream(t *testing.T) {
|
||||
hdr := make(http.Header)
|
||||
hdr.Set("Random-Header", "Value")
|
||||
serverConns, clientURL, close := wireupChannel(t, envTerminalPath, setHeader(hdr), "channel.k8s.io")
|
||||
defer close()
|
||||
|
||||
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
sc := <-serverConns
|
||||
defer sc.conn.Close()
|
||||
require.Equal(t, "Value", sc.req.Header.Get("Random-Header"), "Header specified by upstream not sent to remote")
|
||||
}
|
||||
|
||||
func TestChannelProxyForwardsXForwardedForFromClient(t *testing.T) {
|
||||
serverConns, clientURL, close := wireupChannel(t, envTerminalPath, nil, "channel.k8s.io")
|
||||
defer close()
|
||||
|
||||
hdr := make(http.Header)
|
||||
hdr.Set("X-Forwarded-For", "127.0.0.2")
|
||||
client, _, err := dialWebsocket(clientURL, hdr, "terminal.gitlab.com")
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
clientIP, _, err := net.SplitHostPort(client.LocalAddr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
sc := <-serverConns
|
||||
defer sc.conn.Close()
|
||||
|
||||
require.Equal(t, "127.0.0.2, "+clientIP, sc.req.Header.Get("X-Forwarded-For"), "X-Forwarded-For from client not sent to remote")
|
||||
}
|
||||
|
||||
func wireupChannel(t *testing.T, channelPath string, modifier func(*api.Response), subprotocols ...string) (chan connWithReq, string, func()) {
|
||||
serverConns, remote := startWebsocketServer(subprotocols...)
|
||||
authResponse := channelOkBody(remote, nil, subprotocols...)
|
||||
if modifier != nil {
|
||||
modifier(authResponse)
|
||||
}
|
||||
upstream := testAuthServer(t, nil, nil, 200, authResponse)
|
||||
workhorse := startWorkhorseServer(upstream.URL)
|
||||
|
||||
return serverConns, websocketURL(workhorse.URL, channelPath), func() {
|
||||
workhorse.Close()
|
||||
upstream.Close()
|
||||
remote.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func startWebsocketServer(subprotocols ...string) (chan connWithReq, *httptest.Server) {
|
||||
upgrader := &websocket.Upgrader{Subprotocols: subprotocols}
|
||||
|
||||
connCh := make(chan connWithReq, 1)
|
||||
server := httptest.NewTLSServer(webSocketHandler(upgrader, connCh))
|
||||
|
||||
return connCh, server
|
||||
}
|
||||
|
||||
func webSocketHandler(upgrader *websocket.Upgrader, connCh chan connWithReq) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logEntry := log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"url": r.URL,
|
||||
"headers": r.Header,
|
||||
})
|
||||
|
||||
logEntry.Info("WEBSOCKET")
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
logEntry.WithError(err).Error("WEBSOCKET Upgrade failed")
|
||||
return
|
||||
}
|
||||
connCh <- connWithReq{conn, r}
|
||||
// The connection has been hijacked so it's OK to end here
|
||||
})
|
||||
}
|
||||
|
||||
func channelOkBody(remote *httptest.Server, header http.Header, subprotocols ...string) *api.Response {
|
||||
out := &api.Response{
|
||||
Channel: &api.ChannelSettings{
|
||||
Url: websocketURL(remote.URL),
|
||||
Header: header,
|
||||
Subprotocols: subprotocols,
|
||||
MaxSessionTime: 0,
|
||||
},
|
||||
}
|
||||
|
||||
if len(remote.TLS.Certificates) > 0 {
|
||||
data := bytes.NewBuffer(nil)
|
||||
pem.Encode(data, &pem.Block{Type: "CERTIFICATE", Bytes: remote.TLS.Certificates[0].Certificate[0]})
|
||||
out.Channel.CAPem = data.String()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func badCA(authResponse *api.Response) {
|
||||
authResponse.Channel.CAPem = "Bad CA"
|
||||
}
|
||||
|
||||
func timeout(authResponse *api.Response) {
|
||||
authResponse.Channel.MaxSessionTime = 1
|
||||
}
|
||||
|
||||
func setHeader(hdr http.Header) func(*api.Response) {
|
||||
return func(authResponse *api.Response) {
|
||||
authResponse.Channel.Header = hdr
|
||||
}
|
||||
}
|
||||
|
||||
func dialWebsocket(url string, header http.Header, subprotocols ...string) (*websocket.Conn, *http.Response, error) {
|
||||
dialer := &websocket.Dialer{
|
||||
Subprotocols: subprotocols,
|
||||
}
|
||||
|
||||
return dialer.Dial(url, header)
|
||||
}
|
||||
|
||||
func websocketURL(httpURL string, suffix ...string) string {
|
||||
url, err := url.Parse(httpURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch url.Scheme {
|
||||
case "http":
|
||||
url.Scheme = "ws"
|
||||
case "https":
|
||||
url.Scheme = "wss"
|
||||
default:
|
||||
panic("Unknown scheme: " + url.Scheme)
|
||||
}
|
||||
|
||||
url.Path = path.Join(url.Path, strings.Join(suffix, "/"))
|
||||
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func say(conn *websocket.Conn, message string) error {
|
||||
return conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
}
|
||||
|
||||
func requireReadMessage(t *testing.T, conn *websocket.Conn, expectedMessageType int, expectedData string) {
|
||||
messageType, data, err := conn.ReadMessage()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedMessageType, messageType, "message type")
|
||||
require.Equal(t, expectedData, string(data), "message data")
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := _main(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: fatal: %v\n", os.Args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func _main() error {
|
||||
widthParam := os.Getenv("GL_RESIZE_IMAGE_WIDTH")
|
||||
requestedWidth, err := strconv.Atoi(widthParam)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GL_RESIZE_IMAGE_WIDTH: %w", err)
|
||||
}
|
||||
|
||||
src, formatName, err := image.Decode(os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
imagingFormat, err := imaging.FormatFromExtension(formatName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find imaging format: %w", err)
|
||||
}
|
||||
|
||||
image := imaging.Resize(src, requestedWidth, 0, imaging.Lanczos)
|
||||
return imaging.Encode(os.Stdout, image, imagingFormat)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab-workhorse/internal/zipartifacts"
|
||||
)
|
||||
|
||||
const progName = "gitlab-zip-cat"
|
||||
|
||||
var Version = "unknown"
|
||||
|
||||
var printVersion = flag.Bool("version", false, "Print version and exit")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
version := fmt.Sprintf("%s %s", progName, Version)
|
||||
if *printVersion {
|
||||
fmt.Println(version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
archivePath := os.Getenv("ARCHIVE_PATH")
|
||||
encodedFileName := os.Getenv("ENCODED_FILE_NAME")
|
||||
|
||||
if len(os.Args) != 1 || archivePath == "" || encodedFileName == "" {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s\n", progName)
|
||||
fmt.Fprintf(os.Stderr, "Env: ARCHIVE_PATH=https://path.to/archive.zip or /path/to/archive.zip\n")
|
||||
fmt.Fprintf(os.Stderr, "Env: ENCODED_FILE_NAME=base64-encoded-file-name\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
scrubbedArchivePath := mask.URL(archivePath)
|
||||
|
||||
fileName, err := zipartifacts.DecodeFileEntry(encodedFileName)
|
||||
if err != nil {
|
||||
fatalError(fmt.Errorf("decode entry %q", encodedFileName), err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
archive, err := zipartifacts.OpenArchive(ctx, archivePath)
|
||||
if err != nil {
|
||||
fatalError(errors.New("open archive"), err)
|
||||
}
|
||||
|
||||
file := findFileInZip(fileName, archive)
|
||||
if file == nil {
|
||||
fatalError(fmt.Errorf("find %q in %q: not found", fileName, scrubbedArchivePath), zipartifacts.ErrorCode[zipartifacts.CodeEntryNotFound])
|
||||
}
|
||||
// Start decompressing the file
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
fatalError(fmt.Errorf("open %q in %q", fileName, scrubbedArchivePath), err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if _, err := fmt.Printf("%d\n", file.UncompressedSize64); err != nil {
|
||||
fatalError(fmt.Errorf("write file size invalid"), err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(os.Stdout, reader); err != nil {
|
||||
fatalError(fmt.Errorf("write %q from %q to stdout", fileName, scrubbedArchivePath), err)
|
||||
}
|
||||
}
|
||||
|
||||
func findFileInZip(fileName string, archive *zip.Reader) *zip.File {
|
||||
for _, file := range archive.File {
|
||||
if file.Name == fileName {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fatalError(contextErr error, statusErr error) {
|
||||
code := zipartifacts.ExitCodeByError(statusErr)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s error: %v - %v, code: %d\n", progName, statusErr, contextErr, code)
|
||||
|
||||
if code > 0 {
|
||||
os.Exit(code)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package limit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var ErrLimitExceeded = errors.New("reader limit exceeded")
|
||||
|
||||
const megabyte = 1 << 20
|
||||
|
||||
// LimitedReaderAt supports running a callback in case of reaching a read limit
|
||||
// (bytes), and allows using a smaller limit than a defined offset for a read.
|
||||
type LimitedReaderAt struct {
|
||||
read int64
|
||||
limit int64
|
||||
parent io.ReaderAt
|
||||
limitFunc func(int64)
|
||||
}
|
||||
|
||||
func (r *LimitedReaderAt) ReadAt(p []byte, off int64) (int, error) {
|
||||
if max := r.limit - r.read; int64(len(p)) > max {
|
||||
p = p[0:max]
|
||||
}
|
||||
|
||||
n, err := r.parent.ReadAt(p, off)
|
||||
atomic.AddInt64(&r.read, int64(n))
|
||||
|
||||
if r.read >= r.limit {
|
||||
r.limitFunc(r.read)
|
||||
|
||||
return n, ErrLimitExceeded
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func NewLimitedReaderAt(reader io.ReaderAt, limit int64, limitFunc func(int64)) io.ReaderAt {
|
||||
return &LimitedReaderAt{parent: reader, limit: limit, limitFunc: limitFunc}
|
||||
}
|
||||
|
||||
// SizeToLimit tries to dermine an appropriate limit in bytes for an archive of
|
||||
// a given size. If the size is less than 1 gigabyte we always limit a reader
|
||||
// to 100 megabytes, otherwise the limit is 10% of a given size.
|
||||
func SizeToLimit(size int64) int64 {
|
||||
if size <= 1024*megabyte {
|
||||
return 100 * megabyte
|
||||
}
|
||||
|
||||
return size / 10
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package limit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadAt(t *testing.T) {
|
||||
t.Run("when limit has not been reached", func(t *testing.T) {
|
||||
r := strings.NewReader("some string to read")
|
||||
buf := make([]byte, 11)
|
||||
|
||||
reader := NewLimitedReaderAt(r, 32, func(n int64) {
|
||||
require.Zero(t, n)
|
||||
})
|
||||
p, err := reader.ReadAt(buf, 0)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 11, p)
|
||||
require.Equal(t, "some string", string(buf))
|
||||
})
|
||||
|
||||
t.Run("when read limit is exceeded", func(t *testing.T) {
|
||||
r := strings.NewReader("some string to read")
|
||||
buf := make([]byte, 11)
|
||||
|
||||
reader := NewLimitedReaderAt(r, 9, func(n int64) {
|
||||
require.Equal(t, 9, int(n))
|
||||
})
|
||||
p, err := reader.ReadAt(buf, 0)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 9, p)
|
||||
require.Equal(t, "some stri\x00\x00", string(buf))
|
||||
})
|
||||
|
||||
t.Run("when offset is higher than a limit", func(t *testing.T) {
|
||||
r := strings.NewReader("some string to read")
|
||||
buf := make([]byte, 4)
|
||||
|
||||
reader := NewLimitedReaderAt(r, 5, func(n int64) {
|
||||
require.Zero(t, n)
|
||||
})
|
||||
|
||||
p, err := reader.ReadAt(buf, 15)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, p)
|
||||
require.Equal(t, "read", string(buf))
|
||||
})
|
||||
|
||||
t.Run("when a read starts at the limit", func(t *testing.T) {
|
||||
r := strings.NewReader("some string to read")
|
||||
buf := make([]byte, 11)
|
||||
|
||||
reader := NewLimitedReaderAt(r, 10, func(n int64) {
|
||||
require.Equal(t, 10, int(n))
|
||||
})
|
||||
|
||||
reader.ReadAt(buf, 0)
|
||||
p, err := reader.ReadAt(buf, 0)
|
||||
|
||||
require.EqualError(t, err, ErrLimitExceeded.Error())
|
||||
require.Equal(t, 0, p)
|
||||
require.Equal(t, "some strin\x00", string(buf))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSizeToLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
size int64
|
||||
limit int64
|
||||
name string
|
||||
}{
|
||||
{size: 1, limit: 104857600, name: "1b to 100mb"},
|
||||
{size: 100, limit: 104857600, name: "100b to 100mb"},
|
||||
{size: 104857600, limit: 104857600, name: "100mb to 100mb"},
|
||||
{size: 1073741824, limit: 104857600, name: "1gb to 100mb"},
|
||||
{size: 10737418240, limit: 1073741824, name: "10gb to 1gb"},
|
||||
{size: 53687091200, limit: 5368709120, name: "50gb to 5gb"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require.Equal(t, test.limit, SizeToLimit(test.size))
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue