Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-14 15:10:46 +00:00
parent 18873553de
commit 8106ac487c
95 changed files with 1029 additions and 353 deletions

View file

@ -50,50 +50,6 @@ rules:
# various vue lint rules as they were in eslint-plugin-vue@6, or disabling
# new ones, to ease migration to v7, so violations of each can be fixed
# separately.
vue/order-in-components:
- error
# This is the order from eslint-plugin-vue@6.2.2
- order:
- el
- name
- parent
- functional
-
- delimiters
- comments
-
- components
- directives
- filters
- extends
- mixins
- inheritAttrs
- model
-
- props
- propsData
- fetch
- asyncData
- data
- computed
- watch
-
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- activated
- deactivated
- beforeDestroy
- destroyed
- methods
- head
-
- template
- render
- renderError
vue/no-mutating-props: off
vue/one-component-per-file: off
vue/no-lone-template: off

View file

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.7.4 (2021-01-13)
### Security (1 change)
- Deny implicit flow for confidential apps.
## 13.7.3 (2021-01-08)
### Fixed (7 changes)
@ -497,6 +504,13 @@ entry.
- Update GitLab Workhorse to v8.57.0.
## 13.6.5 (2021-01-13)
### Security (1 change)
- Deny implicit flow for confidential apps.
## 13.6.4 (2021-01-07)
### Security (7 changes)
@ -1068,6 +1082,13 @@ entry.
- Change wording on the project remove fork page. !47878
## 13.5.7 (2021-01-13)
### Security (1 change)
- Deny implicit flow for confidential apps.
## 13.5.6 (2021-01-07)
### Security (7 changes)

View file

@ -1 +1 @@
a6674b359a02a4bf0549dcaa77ac05b1f4850831
3083074640633df94cbeee611795a6fc6d8c5607

View file

@ -465,7 +465,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.7.0.pre.rc1'
gem 'gitaly', '~> 13.8.0.pre.rc2'
gem 'grpc', '~> 1.30.2'

View file

@ -417,7 +417,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
gitaly (13.7.0.pre.rc1)
gitaly (13.8.0.pre.rc2)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
@ -1358,7 +1358,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 13.7.0.pre.rc1)
gitaly (~> 13.8.0.pre.rc2)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-experiment (~> 0.4.5)

View file

@ -5,6 +5,12 @@ import { __ } from '~/locale';
const DEFAULT_PER_PAGE = 20;
/**
* Slow deprecation Notice: Please rather use for new calls
* or during refactors /rest_api as this is doing named exports
* which support treeshaking
*/
const Api = {
DEFAULT_PER_PAGE,
groupsPath: '/api/:version/groups.json',
@ -152,7 +158,10 @@ const Api = {
});
},
// Return groups list. Filtered by query
/**
* @deprecated This method will be removed soon. Use the
* `getGroups` method in `~/rest_api` instead.
*/
groups(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.groupsPath);
return axios
@ -188,7 +197,10 @@ const Api = {
.then(({ data }) => callback(data));
},
// Return projects list. Filtered by query
/**
* @deprecated This method will be removed soon. Use the
* `getProjects` method in `~/rest_api` instead.
*/
projects(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.projectsPath);
const defaults = {
@ -521,6 +533,10 @@ const Api = {
.replace(':namespace_path', namespacePath);
},
/**
* @deprecated This method will be removed soon. Use the
* `getUsers` method in `~/rest_api` instead.
*/
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
@ -532,6 +548,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUser` method in `~/rest_api` instead.
*/
user(id, options) {
const url = Api.buildUrl(this.userPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
@ -539,11 +559,19 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserCounts` method in `~/rest_api` instead.
*/
userCounts() {
const url = Api.buildUrl(this.userCountsPath);
return axios.get(url);
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserStatus` method in `~/rest_api` instead.
*/
userStatus(id, options) {
const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
@ -551,6 +579,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserProjects` method in `~/rest_api` instead.
*/
userProjects(userId, query, options, callback) {
const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
const defaults = {
@ -586,6 +618,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `updateUserStatus` method in `~/rest_api` instead.
*/
postUserStatus({ emoji, message, availability }) {
const url = Api.buildUrl(this.userPostStatusPath);

View file

@ -0,0 +1,5 @@
import { joinPaths } from '../lib/utils/url_utility';
export function buildApiUrl(url) {
return joinPaths('/', gon.relative_url_root || '', url.replace(':version', gon.api_version));
}

View file

@ -0,0 +1 @@
export const DEFAULT_PER_PAGE = 20;

View file

@ -0,0 +1,22 @@
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
const GROUPS_PATH = '/api/:version/groups.json';
export function getGroups(query, options, callback = () => {}) {
const url = buildApiUrl(GROUPS_PATH);
return axios
.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
})
.then(({ data }) => {
callback(data);
return data;
});
}

View file

@ -0,0 +1,27 @@
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
const PROJECTS_PATH = '/api/:version/projects.json';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
const defaults = {
search: query,
per_page: DEFAULT_PER_PAGE,
simple: true,
};
if (gon.current_user_id) {
defaults.membership = true;
}
return axios
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data, headers }) => {
callback(data);
return { data, headers };
});
}

View file

@ -0,0 +1,66 @@
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
const USER_PATH = '/api/:version/users/:id';
const USER_STATUS_PATH = '/api/:version/users/:id/status';
const USER_PROJECTS_PATH = '/api/:version/users/:id/projects';
const USER_POST_STATUS_PATH = '/api/:version/user/status';
export function getUsers(query, options) {
const url = buildApiUrl(USERS_PATH);
return axios.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
});
}
export function getUser(id, options) {
const url = buildApiUrl(USER_PATH).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: options,
});
}
export function getUserCounts() {
const url = buildApiUrl(USER_COUNTS_PATH);
return axios.get(url);
}
export function getUserStatus(id, options) {
const url = buildApiUrl(USER_STATUS_PATH).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: options,
});
}
export function getUserProjects(userId, query, options, callback) {
const url = buildApiUrl(USER_PROJECTS_PATH).replace(':id', userId);
const defaults = {
search: query,
per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
}
export function updateUserStatus({ emoji, message, availability }) {
const url = buildApiUrl(USER_POST_STATUS_PATH);
return axios.put(url, {
emoji,
message,
availability,
});
}

View file

@ -33,13 +33,13 @@ export default {
GlDropdownText,
GlSearchBoxByType,
},
inject: ['groupId'],
props: {
list: {
type: Object,
required: true,
},
},
inject: ['groupId'],
data() {
return {
initialLoading: true,

View file

@ -1,4 +1,4 @@
import Api from '~/api';
import { getUserCounts } from '~/rest_api';
let channel;
@ -30,7 +30,7 @@ function updateMergeRequestCounts(newCount) {
* Refresh user counts (and broadcast if open)
*/
export function refreshUserMergeRequestCounts() {
return Api.userCounts()
return getUserCounts()
.then(({ data }) => {
const assignedMergeRequests = data.assigned_merge_requests;
const reviewerMergeRequests = data.review_requested_merge_requests;

View file

@ -1,7 +1,7 @@
import Api from '~/api';
import AccessorUtilities from '~/lib/utils/accessor';
import * as types from './mutation_types';
import { getTopFrequentItems } from '../utils';
import { getGroups, getProjects } from '~/rest_api';
export const setNamespace = ({ commit }, namespace) => {
commit(types.SET_NAMESPACE, namespace);
@ -54,11 +54,15 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
membership: Boolean(gon.current_user_id),
};
let searchFunction;
if (state.namespace === 'projects') {
searchFunction = getProjects;
params.order_by = 'last_activity_at';
} else {
searchFunction = getGroups;
}
return Api[state.namespace](searchQuery, params)
return searchFunction(searchQuery, params)
.then((results) => {
dispatch('receiveSearchedItemsSuccess', results);
})

View file

@ -3,7 +3,7 @@ import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { USER_SEARCH_DELAY } from '../constants';
import Api from '~/api';
import { getUsers } from '~/rest_api';
export default {
components: {
@ -54,7 +54,7 @@ export default {
this.retrieveUsers(query);
},
retrieveUsers: debounce(function debouncedRetrieveUsers() {
return Api.users(this.query, this.$options.queryOptions)
return getUsers(this.query, this.$options.queryOptions)
.then((response) => {
this.users = response.data.map((token) => ({
id: token.id,

View file

@ -1,4 +1,4 @@
import Api from '../../api';
import { getUsers, getUser, getUserStatus } from '~/rest_api';
import Cache from './cache';
class UsersCache extends Cache {
@ -7,7 +7,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(username));
}
return Api.users('', { username }).then(({ data }) => {
return getUsers('', { username }).then(({ data }) => {
if (!data.length) {
throw new Error(`User "${username}" could not be found!`);
}
@ -28,7 +28,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(userId));
}
return Api.user(userId).then(({ data }) => {
return getUser(userId).then(({ data }) => {
this.internalStorage[userId] = data;
return data;
});
@ -40,7 +40,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(userId).status);
}
return Api.userStatus(userId).then(({ data }) => {
return getUserStatus(userId).then(({ data }) => {
if (!this.hasData(userId)) {
this.internalStorage[userId] = {};
}

View file

@ -559,7 +559,7 @@ export const updateResolvableDiscussionsCounts = ({ commit }) =>
export const submitSuggestion = (
{ commit, dispatch },
{ discussionId, noteId, suggestionId, flashContainer },
{ discussionId, suggestionId, flashContainer },
) => {
const dispatchResolveDiscussion = () =>
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
@ -568,7 +568,6 @@ export const submitSuggestion = (
dispatch('stopPolling');
return Api.applySuggestion(suggestionId)
.then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }))
.then(dispatchResolveDiscussion)
.catch((err) => {
const defaultMessage = __(
@ -590,11 +589,6 @@ export const submitSuggestion = (
export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContainer }) => {
const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId);
const applyAllSuggestions = () =>
state.batchSuggestionsInfo.map((suggestionInfo) =>
commit(types.APPLY_SUGGESTION, suggestionInfo),
);
const resolveAllDiscussions = () =>
state.batchSuggestionsInfo.map((suggestionInfo) => {
const { discussionId } = suggestionInfo;
@ -606,7 +600,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds)
.then(() => Promise.all(applyAllSuggestions()))
.then(() => Promise.all(resolveAllDiscussions()))
.then(() => commit(types.CLEAR_SUGGESTION_BATCH))
.catch((err) => {

View file

@ -11,6 +11,9 @@ import initPerformanceBarLog from './performance_bar_log';
Vue.use(Translate);
const initPerformanceBar = (el) => {
if (!el) {
return undefined;
}
const performanceBarData = el.dataset;
return new Vue({
@ -126,25 +129,7 @@ const initPerformanceBar = (el) => {
});
};
let loadedPeekBar = false;
function loadBar() {
const jsPeek = document.querySelector('#js-peek');
if (!loadedPeekBar && jsPeek) {
loadedPeekBar = true;
initPerformanceBar(jsPeek);
}
}
// If js-peek is not loaded when this script is executed, this call will do nothing
// If this is the case, then it will loadBar on DOMContentLoaded. We would prefer it
// to be initialized before the DOMContetLoaded event in order to pick up all the
// requests sent from the page.
loadBar();
document.addEventListener('DOMContentLoaded', () => {
loadBar();
});
initPerformanceBar(document.querySelector('#js-peek'));
initPerformanceBarLog();
export default initPerformanceBar;

View file

@ -0,0 +1,15 @@
export * from './api/groups_api';
export * from './api/projects_api';
export * from './api/user_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests. See https://stackoverflow.com/a/53307822/1063392.
// As a workaround, in Jest tests, import the methods from the file
// in which they are defined:
//
// import * as UserApi from '~/api/user_api';
// vs...
// import * as UserApi from '~/rest_api';
//
// // This will only work with option #2 above.
// jest.spyOn(UserApi, 'getUsers')

View file

@ -6,7 +6,7 @@ import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import Api from '~/api';
import { updateUserStatus } from '~/rest_api';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy, isValidAvailibility } from './utils';
import * as Emoji from '~/emoji';
@ -163,7 +163,7 @@ export default {
setStatus() {
const { emoji, message, availability } = this;
Api.postUserStatus({
updateUserStatus({
emoji,
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,

View file

@ -191,9 +191,13 @@ export default {
mergeError = mergeError.slice(0, -1);
}
return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), {
mergeError,
});
return sprintf(
s__('mrWidget|Merge failed: %{mergeError}. Please try again.'),
{
mergeError,
},
false,
);
},
shouldShowAccessibilityReport() {
return this.mr.accessibilityReportPath;

View file

@ -57,6 +57,10 @@ class Admin::ProjectsController < Admin::ApplicationController
namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace)
if @project.errors[:new_namespace].present?
flash[:alert] = @project.errors[:new_namespace].first
end
@project.reset
redirect_to admin_project_path(@project)
end

View file

@ -4,7 +4,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
before_action :verify_confirmed_email!
before_action :verify_confirmed_email!, :verify_confidential_application!
layout 'profile'
@ -24,18 +24,19 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
end
end
def create
# Confidential apps require the client_secret to be sent with the request.
# Doorkeeper allows implicit grant flow requests (response_type=token) to
# work without client_secret regardless of the confidential setting.
if pre_auth.authorizable? && pre_auth.response_type == 'token' && pre_auth.client.application.confidential
render "doorkeeper/authorizations/error"
else
super
end
private
# Confidential apps require the client_secret to be sent with the request.
# Doorkeeper allows implicit grant flow requests (response_type=token) to
# work without client_secret regardless of the confidential setting.
# This leads to security vulnerabilities and we want to block it.
def verify_confidential_application!
render 'doorkeeper/authorizations/error' if authorizable_confidential?
end
private
def authorizable_confidential?
pre_auth.authorizable? && pre_auth.response_type == 'token' && pre_auth.client.application.confidential
end
def verify_confirmed_email!
return if current_user&.confirmed?

View file

@ -159,7 +159,7 @@ module PageLayoutHelper
end
def user_status_properties(user)
default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user), default_emoji: UserStatus::DEFAULT_EMOJI }
default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI }
return default_properties unless user&.status
default_properties.merge({

View file

@ -24,9 +24,22 @@ module Ci
def status
strong_memoize(:status) do
status_struct.status
end
end
def success?
status.to_s == 'success'
end
def has_warnings?
status_struct.warnings?
end
def status_struct
strong_memoize(:status_struct) do
Gitlab::Ci::Status::Composite
.new(@jobs)
.status
end
end

View file

@ -34,7 +34,13 @@ module Boards
end
def title
label? ? label.name : list_type.humanize
if label?
label.name
elsif backlog?
_('Open')
else
list_type.humanize
end
end
private

View file

@ -157,8 +157,12 @@ class JiraService < IssueTrackerService
# support any events.
end
def find_issue(issue_key)
jira_request { client.Issue.find(issue_key) }
end
def close_issue(entity, external_issue)
issue = jira_request { client.Issue.find(external_issue.iid) }
issue = find_issue(external_issue.iid)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
@ -172,7 +176,7 @@ class JiraService < IssueTrackerService
# Depending on the Jira project's workflow, a comment during transition
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
end
@ -181,7 +185,7 @@ class JiraService < IssueTrackerService
return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
end
jira_issue = jira_request { client.Issue.find(mentioned.id) }
jira_issue = find_issue(mentioned.id)
return unless jira_issue.present?

View file

@ -12,6 +12,10 @@ module Ci
EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock'
LOCK_TIMEOUT = 6.minutes
def initialize
@removed_artifacts_count = 0
end
##
# Destroy expired job artifacts on GitLab instance
#
@ -20,48 +24,14 @@ module Ci
# which is scheduled every 7 minutes.
def execute
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
if ::Feature.enabled?(:ci_slow_artifacts_removal)
destroy_job_and_pipeline_artifacts
else
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
destroy_artifacts_batch
end
end
destroy_job_artifacts_with_slow_iteration(Time.current)
end
@removed_artifacts_count
end
private
def destroy_job_and_pipeline_artifacts
start_at = Time.current
destroy_job_artifacts_with_slow_iteration(start_at)
timeout = LOOP_TIMEOUT - (Time.current - start_at)
return false if timeout < 0
loop_until(timeout: timeout, limit: LOOP_LIMIT) do
destroy_pipeline_artifacts_batch
end
end
def destroy_artifacts_batch
destroy_job_artifacts_batch || destroy_pipeline_artifacts_batch
end
def destroy_job_artifacts_batch
artifacts = Ci::JobArtifact
.expired(BATCH_SIZE)
.unlocked
.order_expired_desc
.with_destroy_preloads
.to_a
return false if artifacts.empty?
parallel_destroy_batch(artifacts)
true
end
def destroy_job_artifacts_with_slow_iteration(start_at)
Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index|
artifacts = relation.unlocked.with_destroy_preloads.to_a
@ -72,19 +42,6 @@ module Ci
end
end
# TODO: Make sure this can also be parallelized
# https://gitlab.com/gitlab-org/gitlab/-/issues/270973
def destroy_pipeline_artifacts_batch
return false if ::Feature.enabled?(:ci_split_pipeline_artifacts_removal)
artifacts = Ci::PipelineArtifact.expired(BATCH_SIZE).to_a
return false if artifacts.empty?
artifacts.each(&:destroy!)
true
end
def parallel_destroy_batch(job_artifacts)
Ci::DeletedObject.transaction do
Ci::DeletedObject.bulk_import(job_artifacts)
@ -93,14 +50,14 @@ module Ci
end
# This is executed outside of the transaction because it depends on Redis
update_statistics_for(job_artifacts)
destroyed_artifacts_counter.increment({}, job_artifacts.size)
update_project_statistics_for(job_artifacts)
increment_monitoring_statistics(job_artifacts.size)
end
# This method is implemented in EE and it must do only database work
def destroy_related_records_for(job_artifacts); end
def update_statistics_for(job_artifacts)
def update_project_statistics_for(job_artifacts)
artifacts_by_project = job_artifacts.group_by(&:project)
artifacts_by_project.each do |project, artifacts|
delta = -artifacts.sum { |artifact| artifact.size.to_i }
@ -109,6 +66,11 @@ module Ci
end
end
def increment_monitoring_statistics(size)
destroyed_artifacts_counter.increment({}, size)
@removed_artifacts_count += size
end
def destroyed_artifacts_counter
strong_memoize(:destroyed_artifacts_counter) do
name = :destroyed_job_artifacts_count_total

View file

@ -8,6 +8,10 @@ module DraftNotes
@merge_request, @current_user, @params = merge_request, current_user, params.dup
end
def merge_request_activity_counter
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
end
private
def draft_notes

View file

@ -31,6 +31,10 @@ module DraftNotes
merge_request.diffs.clear_cache
end
if draft_note.persisted?
merge_request_activity_counter.track_create_review_note_action(user: current_user)
end
draft_note
end

View file

@ -9,6 +9,7 @@ module DraftNotes
publish_draft_note(draft)
else
publish_draft_notes
merge_request_activity_counter.track_publish_review_action(user: current_user)
end
success

View file

@ -70,7 +70,7 @@
prepend: emoji_button,
append: reset_message_button,
placeholder: s_("Profiles|What's your status?")
- if Feature.enabled?(:set_user_availability_status, @user)
- if Feature.enabled?(:set_user_availability_status, @user, default_enabled: :yaml)
.checkbox-icon-inline-wrapper
= status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
.gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name')

View file

@ -1,3 +1,5 @@
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- @content_class = "limit-container-width" unless fluid_layout
- @skip_current_level_breadcrumb = true

View file

@ -1,6 +1,6 @@
- button_path = local_assigns.fetch(:button_path, false)
.row.empty-state.mt-0
.row.empty-state
.col-12
.svg-content
= image_tag 'illustrations/snippets_empty.svg', data: { qa_selector: 'svg_content' }
@ -16,5 +16,3 @@
= link_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), class: 'btn btn-default', title: s_('SnippetsEmptyState|Documentation')
- else
%h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.')

View file

@ -14,8 +14,6 @@ module Ci
feature_category :continuous_integration
def perform
return unless ::Feature.enabled?(:ci_split_pipeline_artifacts_removal)
service = ::Ci::PipelineArtifacts::DestroyExpiredArtifactsService.new
artifacts_count = service.execute
log_extra_metadata_on_done(:destroyed_pipeline_artifacts_count, artifacts_count)

View file

@ -10,6 +10,8 @@ class ExpireBuildArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :continuous_integration
def perform
Ci::DestroyExpiredJobArtifactsService.new.execute
service = Ci::DestroyExpiredJobArtifactsService.new
artifacts_count = service.execute
log_extra_metadata_on_done(:destroyed_job_artifacts_count, artifacts_count)
end
end

View file

@ -0,0 +1,5 @@
---
title: Correct status indicator for jobs groups when failure is allowed
merge_request: 51478
author: Sune Keller (sirlatrom)
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Enables the CI Pipeline Editor feature as a way to edit the GitLab CI/CD configuration
merge_request: 51484
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Fix multiple errors in custom server hook render improperly
merge_request: 51001
author: Kev @KevSlashNull
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add metrics to starting and publishing a review
merge_request: 51521
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Flash transfer errors in the admin project controller
merge_request: 50541
author: Vincent Fazio
type: fixed

View file

@ -0,0 +1,6 @@
---
title: Extract expired pipeline artifacts removal service into it's own background
worker
merge_request: 51323
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Default enable set_user_availability_status
merge_request: 51668
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add documentation for new Snippet repository storage move API
merge_request: 50151
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Fixed applied message showing before discussion gets resolved
merge_request: 51605
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Rename Backlog list to Open in issue boards
merge_request: 51562
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Update Terraform Pipline templaes to support 0.14 lockfile cache
merge_request: 50647
author: Aurelian Shuttleworth
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Fix slack application helper card
merge_request: 51034
author: Yogi (@yo)
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Remove margin top for snippets empty state
merge_request: 51038
author: Yogi (@yo)
type: fixed

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/270059
milestone: '13.6'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View file

@ -1,8 +0,0 @@
---
name: ci_slow_artifacts_removal
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47496
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281688
milestone: '13.8'
type: development
group: 'group::continuous integration'
default_enabled: false

View file

@ -1,8 +0,0 @@
---
name: ci_split_pipeline_artifacts_removal
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50446
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/295300
milestone: '13.8'
type: development
group: group::continuous integration
default_enabled: false

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281073
milestone: '13.6'
type: development
group: group::optimize
default_enabled: false
default_enabled: true

View file

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_create_review_note
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51521
rollout_issue_url:
milestone: '13.8'
type: development
group: group::code review
default_enabled: true

View file

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_publish_review
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51351
rollout_issue_url:
milestone: '13.8'
type: development
group: group::code review
default_enabled: true

View file

@ -1363,7 +1363,7 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
If your GitLab instance already has repositories on single Gitaly nodes, these aren't migrated to
Gitaly Cluster automatically.
Repositories may be moved from one storage location using the [Project repository storage moves API](../../api/project_repository_storage_moves.md):
Project repositories may be moved from one storage location using the [Project repository storage moves API](../../api/project_repository_storage_moves.md):
NOTE:
The Project repository storage moves API [cannot move all repository types](../../api/project_repository_storage_moves.md#limitations).
@ -1387,6 +1387,8 @@ To move repositories to Gitaly Cluster:
using the API to confirm that all projects have moved. No projects should be returned
with `repository_storage` field set to the old storage.
In a similar way, you can move Snippet repositories using the [Snippet repository storage moves API](../../api/snippet_repository_storage_moves.md):
## Debugging Praefect
If you receive an error, check `/var/log/gitlab/gitlab-rails/production.log`.

View file

@ -23,12 +23,14 @@ For more information, see:
- [Configuring additional storage for Gitaly](../gitaly/index.md#network-architecture). Within this
example, additional storage called `storage1` and `storage2` is configured.
- [The API documentation](../../api/project_repository_storage_moves.md) details the endpoints for
querying and scheduling repository moves.
querying and scheduling project repository moves.
- [The API documentation](../../api/snippet_repository_storage_moves.md) details the endpoints for
querying and scheduling snippet repository moves.
- [Migrate existing repositories to Gitaly Cluster](../gitaly/praefect.md#migrate-existing-repositories-to-gitaly-cluster).
### Limitations
Read more in the [API documentation](../../api/project_repository_storage_moves.md#limitations).
Read more in the [API documentation for projects](../../api/project_repository_storage_moves.md#limitations) and the [API documentation for snippets](../../api/snippet_repository_storage_moves.md#limitations).
## Migrating to another GitLab instance

View file

@ -158,6 +158,7 @@ The following API resources are available outside of project and group contexts
| [Runners](runners.md) | `/runners` (also available for projects) |
| [Search](search.md) | `/search` (also available for groups and projects) |
| [Settings](settings.md) **(CORE ONLY)** | `/application/settings` |
| [Snippet repository storage moves](snippet_repository_storage_moves.md) **(CORE ONLY)** | `/snippet_repository_storage_moves` |
| [Statistics](statistics.md) | `/application/statistics` |
| [Sidekiq metrics](sidekiq_metrics.md) **(CORE ONLY)** | `/sidekiq` |
| [Suggestions](suggestions.md) | `/suggestions` |

View file

@ -30,9 +30,10 @@ read-only. Please try again later.` message if they try to push new commits.
This API requires you to [authenticate yourself](README.md#authentication) as an administrator.
Snippet repositories can be moved using the [Snippet repository storage moves API](snippet_repository_storage_moves.md).
## Limitations
- The repositories associated with snippets [can't be moved with the API](https://gitlab.com/groups/gitlab-org/-/epics/3393).
- Group-level wikis [can't be moved with the API](https://gitlab.com/gitlab-org/gitlab/-/issues/219003).
## Retrieve all project repository storage moves

View file

@ -0,0 +1,293 @@
---
stage: Create
group: Editor
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Snippet repository storage moves API **(CORE ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49228) in GitLab 13.8.
Snippet repositories can be moved between storages. This can be useful when
[migrating to Gitaly Cluster](../administration/gitaly/praefect.md#migrate-existing-repositories-to-gitaly-cluster),
for example.
As snippet repository storage moves are processed, they transition through different states. Values
of `state` are:
- `initial`
- `scheduled`
- `started`
- `finished`
- `failed`
- `replicated`
- `cleanup failed`
To ensure data integrity, snippets are put in a temporary read-only state for the
duration of the move. During this time, users receive a `The repository is temporarily
read-only. Please try again later.` message if they try to push new commits.
This API requires you to [authenticate yourself](README.md#authentication) as an administrator.
Project repositories can be moved using the [Project repository storage moves API](project_repository_storage_moves.md).
## Limitations
- Group-level wikis [can't be moved with the API](https://gitlab.com/gitlab-org/gitlab/-/issues/219003).
## Retrieve all snippet repository storage moves
```plaintext
GET /snippet_repository_storage_moves
```
By default, `GET` requests return 20 results at a time because the API results
are [paginated](README.md#pagination).
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippet_repository_storage_moves"
```
Example response:
```json
[
{
"id": 1,
"created_at": "2020-05-07T04:27:17.234Z",
"state": "scheduled",
"source_storage_name": "default",
"destination_storage_name": "storage2",
"snippet": {
"id": 65,
"title": "Test Snippet",
"description": null,
"visibility": "internal",
"updated_at": "2020-12-01T11:15:50.385Z",
"created_at": "2020-12-01T11:15:50.385Z",
"project_id": null,
"web_url": "https://gitlab.example.com/-/snippets/65",
"raw_url": "https://gitlab.example.com/-/snippets/65/raw",
"ssh_url_to_repo": "ssh://user@gitlab.example.com/snippets/65.git",
"http_url_to_repo": "https://gitlab.example.com/snippets/65.git"
}
}
]
```
## Retrieve all repository storage moves for a snippet
```plaintext
GET /snippets/:snippet_id/repository_storage_moves
```
By default, `GET` requests return 20 results at a time because the API results
are [paginated](README.md#pagination).
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `snippet_id` | integer | yes | ID of the snippet. |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippets/1/repository_storage_moves"
```
Example response:
```json
[
{
"id": 1,
"created_at": "2020-05-07T04:27:17.234Z",
"state": "scheduled",
"source_storage_name": "default",
"destination_storage_name": "storage2",
"snippet": {
"id": 65,
"title": "Test Snippet",
"description": null,
"visibility": "internal",
"updated_at": "2020-12-01T11:15:50.385Z",
"created_at": "2020-12-01T11:15:50.385Z",
"project_id": null,
"web_url": "https://gitlab.example.com/-/snippets/65",
"raw_url": "https://gitlab.example.com/-/snippets/65/raw",
"ssh_url_to_repo": "ssh://user@gitlab.example.com/snippets/65.git",
"http_url_to_repo": "https://gitlab.example.com/snippets/65.git"
}
}
]
```
## Get a single snippet repository storage move
```plaintext
GET /snippet_repository_storage_moves/:repository_storage_id
```
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `repository_storage_id` | integer | yes | ID of the snippet repository storage move. |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippet_repository_storage_moves/1"
```
Example response:
```json
{
"id": 1,
"created_at": "2020-05-07T04:27:17.234Z",
"state": "scheduled",
"source_storage_name": "default",
"destination_storage_name": "storage2",
"snippet": {
"id": 65,
"title": "Test Snippet",
"description": null,
"visibility": "internal",
"updated_at": "2020-12-01T11:15:50.385Z",
"created_at": "2020-12-01T11:15:50.385Z",
"project_id": null,
"web_url": "https://gitlab.example.com/-/snippets/65",
"raw_url": "https://gitlab.example.com/-/snippets/65/raw",
"ssh_url_to_repo": "ssh://user@gitlab.example.com/snippets/65.git",
"http_url_to_repo": "https://gitlab.example.com/snippets/65.git"
}
}
```
## Get a single repository storage move for a snippet
```plaintext
GET /snippets/:snippet_id/repository_storage_moves/:repository_storage_id
```
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `snippet_id` | integer | yes | ID of the snippet. |
| `repository_storage_id` | integer | yes | ID of the snippet repository storage move. |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippets/1/repository_storage_moves/1"
```
Example response:
```json
{
"id": 1,
"created_at": "2020-05-07T04:27:17.234Z",
"state": "scheduled",
"source_storage_name": "default",
"destination_storage_name": "storage2",
"snippet": {
"id": 65,
"title": "Test Snippet",
"description": null,
"visibility": "internal",
"updated_at": "2020-12-01T11:15:50.385Z",
"created_at": "2020-12-01T11:15:50.385Z",
"project_id": null,
"web_url": "https://gitlab.example.com/-/snippets/65",
"raw_url": "https://gitlab.example.com/-/snippets/65/raw",
"ssh_url_to_repo": "ssh://user@gitlab.example.com/snippets/65.git",
"http_url_to_repo": "https://gitlab.example.com/snippets/65.git"
}
}
```
## Schedule a repository storage move for a snippet
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49228) in GitLab 13.8.
```plaintext
POST /snippets/:snippet_id/repository_storage_moves
```
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `snippet_id` | integer | yes | ID of the snippet. |
| `destination_storage_name` | string | no | Name of the destination storage shard. In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitaly/-/issues/3209), the storage is selected automatically if not provided. |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
--data '{"destination_storage_name":"storage2"}' "https://gitlab.example.com/api/v4/snippets/1/repository_storage_moves"
```
Example response:
```json
{
"id": 1,
"created_at": "2020-05-07T04:27:17.234Z",
"state": "scheduled",
"source_storage_name": "default",
"destination_storage_name": "storage2",
"snippet": {
"id": 65,
"title": "Test Snippet",
"description": null,
"visibility": "internal",
"updated_at": "2020-12-01T11:15:50.385Z",
"created_at": "2020-12-01T11:15:50.385Z",
"project_id": null,
"web_url": "https://gitlab.example.com/-/snippets/65",
"raw_url": "https://gitlab.example.com/-/snippets/65/raw",
"ssh_url_to_repo": "ssh://user@gitlab.example.com/snippets/65.git",
"http_url_to_repo": "https://gitlab.example.com/snippets/65.git"
}
}
```
## Schedule repository storage moves for all snippets on a storage shard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49228) in GitLab 13.8.
Schedules repository storage moves for each snippet repository stored on the source storage shard.
```plaintext
POST /snippet_repository_storage_moves
```
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `source_storage_name` | string | yes | Name of the source storage shard. |
| `destination_storage_name` | string | no | Name of the destination storage shard. The storage is selected automatically if not provided. |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
--data '{"source_storage_name":"default"}' "https://gitlab.example.com/api/v4/snippet_repository_storage_moves"
```
Example response:
```json
{
"message": "202 Accepted"
}
```

View file

@ -8,8 +8,8 @@ type: reference
# Pipeline Editor **(CORE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4540) in GitLab 13.8.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-pipeline-editor). **(CORE ONLY)**
@ -115,18 +115,18 @@ checkbox appears. Select it to start a new merge request after you commit the ch
## Enable or disable pipeline editor **(CORE ONLY)**
The pipeline editor is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:ci_pipeline_editor_page)
```
can disable it.
To disable it:
```ruby
Feature.disable(:ci_pipeline_editor_page)
```
To enable it:
```ruby
Feature.enable(:ci_pipeline_editor_page)
```

View file

@ -339,6 +339,7 @@ experience, refactors the existing code). Then:
convey your intent.
- For non-mandatory suggestions, decorate with (non-blocking) so the author knows they can
optionally resolve within the merge request or follow-up at a later stage.
- There's a [Chrome/Firefox addon](https://gitlab.com/conventionalcomments/conventional-comments-button) which you can use to apply [Conventional Comment](https://conventionalcomments.org/) prefixes.
- After a round of line notes, it can be helpful to post a summary note such as
"Looks good to me", or "Just a couple things to address."
- Assign the merge request to the author if changes are required following your

View file

@ -104,7 +104,14 @@ someActionFunction() {
## Extensions
Editor Lite has been built to provide a universal, extensible editing tool to the whole product, which would not depend on any particular group. Even though the Editor Lite's core is owned by [Create::Editor FE Team](https://about.gitlab.com/handbook/engineering/development/dev/create-editor-fe/), the main functional elements — extensions — can be owned by any group. Editor Lite extensions' main idea is that the core of the editor remains very slim and stable. At the same time, whatever new functionality is needed can be added as an extension to this core, without touching the core itself. It allows any group to build and own any new editing functionality without being afraid of it being broken or overridden with the Editor Lite changes.
Editor Lite has been built to provide a universal, extensible editing tool to the whole product,
which would not depend on any particular group. Even though the Editor Lite's core is owned by
[Create::Editor FE Team](https://about.gitlab.com/handbook/engineering/development/dev/create-editor/),
the main functional elements — extensions — can be owned by any group. Editor Lite extensions' main idea
is that the core of the editor remains very slim and stable. At the same time, whatever new functionality
is needed can be added as an extension to this core, without touching the core itself. It allows any group
to build and own any new editing functionality without being afraid of it being broken or overridden with
the Editor Lite changes.
Structurally, the complete implementation of Editor Lite could be presented as the following diagram:

View file

@ -203,11 +203,12 @@ If you previously selected the "Busy" checkbox, remember to deselect it when you
## Busy status indicator
> - Introduced in GitLab 13.6.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259649) in GitLab 13.6.
> - It was [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/281073) in GitLab 13.8.
> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-busy-status-feature).
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-busy-status-feature).
To indicate to others that you are busy, you can set an indicator
@ -228,10 +229,16 @@ To set the busy status indicator, either:
1. Click **Edit profile** (**{pencil}**).
1. Select the **Busy** checkbox
### Enable busy status feature
### Disable busy status feature
The busy status feature is deployed behind a feature flag and is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) can enable it for your instance from the [rails console](../../administration/feature_flags.md#start-the-gitlab-rails-console).
The busy status feature is deployed behind a feature flag and is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) can disable it for your instance from the [rails console](../../administration/feature_flags.md#start-the-gitlab-rails-console).
To disable it:
```ruby
Feature.disable(:set_user_availability_status)
```
To enable it:

View file

@ -62,6 +62,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/publish` | ✓ | | | Publish issue to an associated [Status Page](../../operations/incident_management/status_page.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) **(ULTIMATE)** |
| `/reassign @user1 @user2` | ✓ | ✓ | | Replace current assignees with those specified. **(STARTER)** |
| `/rebase` | | ✓ | | Rebase source branch. This will schedule a background task that attempt to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` will be ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. |
| `/reassign_reviewer @user1 @user2` | | ✓ | | Replace current reviewers with those specified. **(STARTER)** |
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related. **(STARTER)** |
| `/remove_child_epic <epic>` | | | ✓ | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). **(ULTIMATE)** |

View file

@ -60,7 +60,7 @@ module Gitlab
end
def self.ci_pipeline_editor_page_enabled?(project)
::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false)
::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: :yaml)
end
def self.allow_failure_with_exit_codes_enabled?

View file

@ -8,6 +8,10 @@ module Gitlab
def self.common_helpers
Status::Group::Common
end
def self.extended_statuses
[[Status::SuccessWarning]]
end
end
end
end

View file

@ -17,6 +17,7 @@ variables:
cache:
paths:
- .terraform
- .terraform.lock.hcl
before_script:
- alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"

View file

@ -19,6 +19,7 @@ cache:
key: "${TF_ROOT}"
paths:
- ${TF_ROOT}/.terraform/
- ${TF_ROOT}/.terraform.lock.hcl
.init: &init
stage: init

View file

@ -801,7 +801,8 @@ module Gitlab
# forced - should we use --force flag?
# no_tags - should we use --no-tags flag?
# prune - should we use --prune flag?
def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
# check_tags_changed - should we ask gitaly to calculate whether any tags changed?
def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true, check_tags_changed: false)
wrapped_gitaly_errors do
gitaly_repository_client.fetch_remote(
remote,
@ -809,6 +810,7 @@ module Gitlab
forced: forced,
no_tags: no_tags,
prune: prune,
check_tags_changed: check_tags_changed,
timeout: GITLAB_PROJECTS_TIMEOUT
)
end

View file

@ -70,10 +70,11 @@ module Gitlab
end.join
end
def fetch_remote(remote, ssh_auth:, forced:, no_tags:, timeout:, prune: true)
def fetch_remote(remote, ssh_auth:, forced:, no_tags:, timeout:, prune: true, check_tags_changed: false)
request = Gitaly::FetchRemoteRequest.new(
repository: @gitaly_repo, remote: remote, force: forced,
no_tags: no_tags, timeout: timeout, no_prune: !prune
no_tags: no_tags, timeout: timeout, no_prune: !prune,
check_tags_changed: check_tags_changed
)
if ssh_auth&.ssh_mirror_url?

View file

@ -486,6 +486,16 @@
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_remove_mr_comment
- name: i_code_review_user_create_review_note
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_create_review_note
- name: i_code_review_user_publish_review
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_publish_review
# Terraform
- name: p_terraform_state_api_unique_users
category: terraform

View file

@ -13,6 +13,8 @@ module Gitlab
MR_CREATE_COMMENT_ACTION = 'i_code_review_user_create_mr_comment'
MR_EDIT_COMMENT_ACTION = 'i_code_review_user_edit_mr_comment'
MR_REMOVE_COMMENT_ACTION = 'i_code_review_user_remove_mr_comment'
MR_CREATE_REVIEW_NOTE_ACTION = 'i_code_review_user_create_review_note'
MR_PUBLISH_REVIEW_ACTION = 'i_code_review_user_publish_review'
class << self
def track_mr_diffs_action(merge_request:)
@ -52,6 +54,14 @@ module Gitlab
track_unique_action_by_user(MR_REMOVE_COMMENT_ACTION, user)
end
def track_create_review_note_action(user:)
track_unique_action_by_user(MR_CREATE_REVIEW_NOTE_ACTION, user)
end
def track_publish_review_action(user:)
track_unique_action_by_user(MR_PUBLISH_REVIEW_ACTION, user)
end
private
def track_unique_action_by_merge_request(action, merge_request)

View file

@ -5218,6 +5218,12 @@ msgstr ""
msgid "Change permissions"
msgstr ""
msgid "Change reviewer(s)"
msgstr ""
msgid "Change reviewer(s)."
msgstr ""
msgid "Change status"
msgstr ""
@ -5284,6 +5290,9 @@ msgstr ""
msgid "Changed assignee(s)."
msgstr ""
msgid "Changed reviewer(s)."
msgstr ""
msgid "Changed the title to \"%{title_param}\"."
msgstr ""
@ -25263,6 +25272,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
msgid "See the list of available commands in Slack after setting up this service by entering"
msgstr ""
msgid "See vulnerability %{vulnerability_link} for any Remediation details."
msgstr ""
@ -29078,6 +29090,9 @@ msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
msgid "This service allows users to perform common operations on this project by entering slash commands in Slack."
msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""

View file

@ -77,4 +77,34 @@ RSpec.describe Admin::ProjectsController do
expect(response.body).to match(project.name)
end
end
describe 'PUT /projects/transfer/:id' do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:new_namespace) { create(:namespace) }
it 'updates namespace' do
put :transfer, params: { namespace_id: project.namespace.path, new_namespace_id: new_namespace.id, id: project.path }
project.reload
expect(project.namespace).to eq(new_namespace)
expect(response).to have_gitlab_http_status(:redirect)
expect(response).to redirect_to(admin_project_path(project))
end
context 'when project transfer fails' do
it 'flashes error' do
old_namespace = project.namespace
put :transfer, params: { namespace_id: old_namespace.path, new_namespace_id: nil, id: project.path }
project.reload
expect(project.namespace).to eq(old_namespace)
expect(response).to have_gitlab_http_status(:redirect)
expect(response).to redirect_to(admin_project_path(project))
expect(flash[:alert]).to eq s_('TransferProject|Please select a new namespace for your project.')
end
end
end
end

View file

@ -51,10 +51,27 @@ RSpec.describe Oauth::AuthorizationsController do
end
end
shared_examples "Implicit grant can't be used in confidential application" do
context 'when application is confidential' do
before do
application.update(confidential: true)
params[:response_type] = 'token'
end
it 'does not allow the implicit flow' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/error')
end
end
end
describe 'GET #new' do
subject { get :new, params: params }
include_examples 'OAuth Authorizations require confirmed user'
include_examples "Implicit grant can't be used in confidential application"
context 'when the user is confirmed' do
let(:confirmed_at) { 1.hour.ago }
@ -95,26 +112,14 @@ RSpec.describe Oauth::AuthorizationsController do
subject { post :create, params: params }
include_examples 'OAuth Authorizations require confirmed user'
context 'when application is confidential' do
before do
application.update(confidential: true)
params[:response_type] = 'token'
end
it 'does not allow the implicit flow' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/error')
end
end
include_examples "Implicit grant can't be used in confidential application"
end
describe 'DELETE #destroy' do
subject { delete :destroy, params: params }
include_examples 'OAuth Authorizations require confirmed user'
include_examples "Implicit grant can't be used in confidential application"
end
it 'includes Two-factor enforcement concern' do

View file

@ -733,7 +733,7 @@ RSpec.describe ProjectsController do
describe '#transfer', :enable_admin_mode do
render_views
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:admin) { create(:admin) }
let_it_be(:new_namespace) { create(:namespace) }

View file

@ -0,0 +1,35 @@
import * as apiUtils from '~/api/api_utils';
describe('~/api/api_utils.js', () => {
describe('buildApiUrl', () => {
beforeEach(() => {
window.gon = {
api_version: 'v7',
};
});
it('returns a URL with the correct API version', () => {
expect(apiUtils.buildApiUrl('/api/:version/users/:id/status')).toEqual(
'/api/v7/users/:id/status',
);
});
it('only replaces the first instance of :version in the URL', () => {
expect(apiUtils.buildApiUrl('/api/:version/projects/:id/packages/:version')).toEqual(
'/api/v7/projects/:id/packages/:version',
);
});
describe('when gon includes a relative_url_root property', () => {
beforeEach(() => {
window.gon.relative_url_root = '/relative/root';
});
it('returns a URL with the correct relative root URL and API version', () => {
expect(apiUtils.buildApiUrl('/api/:version/users/:id/status')).toEqual(
'/relative/root/api/v7/users/:id/status',
);
});
});
});
});

View file

@ -3,7 +3,7 @@ import {
closeUserCountsBroadcast,
refreshUserMergeRequestCounts,
} from '~/commons/nav/user_merge_requests';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
jest.mock('~/api');
@ -33,14 +33,12 @@ describe('User Merge Requests', () => {
describe('refreshUserMergeRequestCounts', () => {
beforeEach(() => {
Api.userCounts.mockReturnValue(
Promise.resolve({
data: {
assigned_merge_requests: TEST_COUNT,
review_requested_merge_requests: TEST_COUNT,
},
}),
);
jest.spyOn(UserApi, 'getUserCounts').mockResolvedValue({
data: {
assigned_merge_requests: TEST_COUNT,
review_requested_merge_requests: TEST_COUNT,
},
});
});
describe('with open broadcast channel', () => {
@ -55,7 +53,7 @@ describe('User Merge Requests', () => {
});
it('calls the API', () => {
expect(Api.userCounts).toHaveBeenCalled();
expect(UserApi.getUserCounts).toHaveBeenCalled();
});
it('posts count to BroadcastChannel', () => {

View file

@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import { GlTokenSelector } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
const label = 'testgroup';
@ -28,7 +28,7 @@ describe('MembersTokenSelect', () => {
let wrapper;
beforeEach(() => {
jest.spyOn(Api, 'users').mockResolvedValue({ data: allUsers });
jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers });
wrapper = createComponent();
});
@ -57,7 +57,7 @@ describe('MembersTokenSelect', () => {
await waitForPromises();
expect(Api.users).not.toHaveBeenCalled();
expect(UserApi.getUsers).not.toHaveBeenCalled();
});
});
@ -90,7 +90,10 @@ describe('MembersTokenSelect', () => {
await waitForPromises();
expect(Api.users).toHaveBeenCalledWith(searchParam, wrapper.vm.$options.queryOptions);
expect(UserApi.getUsers).toHaveBeenCalledWith(
searchParam,
wrapper.vm.$options.queryOptions,
);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});

View file

@ -1,4 +1,4 @@
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
@ -88,7 +88,9 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'users').mockImplementation((query, options) => apiSpy(query, options));
jest
.spyOn(UserApi, 'getUsers')
.mockImplementation((query, options) => apiSpy(query, options));
});
it('stores and returns data from API call if cache is empty', (done) => {
@ -151,7 +153,7 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'user').mockImplementation((id) => apiSpy(id));
jest.spyOn(UserApi, 'getUser').mockImplementation((id) => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', (done) => {
@ -208,7 +210,7 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'userStatus').mockImplementation((id) => apiSpy(id));
jest.spyOn(UserApi, 'getUserStatus').mockImplementation((id) => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', (done) => {

View file

@ -918,7 +918,6 @@ describe('Actions Notes Store', () => {
testSubmitSuggestion(done, () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
@ -1001,8 +1000,6 @@ describe('Actions Notes Store', () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[0]],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[1]],
[mutationTypes.CLEAR_SUGGESTION_BATCH],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
@ -1066,8 +1063,6 @@ describe('Actions Notes Store', () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[0]],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[1]],
[mutationTypes.CLEAR_SUGGESTION_BATCH],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],

View file

@ -4,25 +4,23 @@ import '~/performance_bar/components/performance_bar_app.vue';
import performanceBar from '~/performance_bar';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
jest.mock('~/performance_bar/performance_bar_log');
describe('performance bar wrapper', () => {
let mock;
let vm;
beforeEach(() => {
setFixtures('<div id="js-peek"></div>');
const peekWrapper = document.getElementById('js-peek');
performance.getEntriesByType = jest.fn().mockReturnValue([]);
// clear html so that elements from previous tests don't mess with this test
document.body.innerHTML = '';
const peekWrapper = document.createElement('div');
peekWrapper.setAttribute('id', 'js-peek');
peekWrapper.setAttribute('data-env', 'development');
peekWrapper.setAttribute('data-request-id', '123');
peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
document.body.appendChild(peekWrapper);
mock = new MockAdapter(axios);
mock.onGet('/-/peek/results').reply(
@ -48,6 +46,7 @@ describe('performance bar wrapper', () => {
afterEach(() => {
vm.$destroy();
document.getElementById('js-peek').remove();
mock.restore();
});

View file

@ -1,13 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { initEmojiMock } from 'helpers/emoji';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import SetStatusModalWrapper, {
AVAILABILITY_STATUS,
} from '~/set_status_modal/set_status_modal_wrapper.vue';
jest.mock('~/api');
jest.mock('~/flash');
describe('SetStatusModalWrapper', () => {
@ -150,7 +149,7 @@ describe('SetStatusModalWrapper', () => {
describe('update status', () => {
describe('succeeds', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
});
it('clicking "removeStatus" clears the emoji and message fields', async () => {
@ -173,12 +172,12 @@ describe('SetStatusModalWrapper', () => {
const commonParams = { emoji: defaultEmoji, message: defaultMessage };
expect(Api.postUserStatus).toHaveBeenCalledTimes(2);
expect(Api.postUserStatus).toHaveBeenNthCalledWith(1, {
expect(UserApi.updateUserStatus).toHaveBeenCalledTimes(2);
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(1, {
availability: AVAILABILITY_STATUS.NOT_SET,
...commonParams,
});
expect(Api.postUserStatus).toHaveBeenNthCalledWith(2, {
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(2, {
availability: AVAILABILITY_STATUS.BUSY,
...commonParams,
});
@ -196,7 +195,7 @@ describe('SetStatusModalWrapper', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
return initModal({ mockOnUpdateSuccess: false });
});
@ -210,7 +209,7 @@ describe('SetStatusModalWrapper', () => {
describe('with errors', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
});
it('calls the "onUpdateFail" handler', async () => {
@ -225,7 +224,7 @@ describe('SetStatusModalWrapper', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
return initModal({ mockOnUpdateFailure: false });
});

View file

@ -12,4 +12,9 @@ RSpec.describe Gitlab::Ci::Status::Group::Factory do
expect(described_class.common_helpers)
.to eq Gitlab::Ci::Status::Group::Common
end
it 'exposes extended statuses' do
expect(described_class.extended_statuses)
.to eq([[Gitlab::Ci::Status::SuccessWarning]])
end
end

View file

@ -520,12 +520,13 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
forced: true,
no_tags: true,
timeout: described_class::GITLAB_PROJECTS_TIMEOUT,
prune: false
prune: false,
check_tags_changed: false
}
expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts)
repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false)
repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false, check_tags_changed: false)
end
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do

View file

@ -131,7 +131,8 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
known_hosts: '',
force: false,
no_tags: false,
no_prune: false
no_prune: false,
check_tags_changed: false
)
expect_any_instance_of(Gitaly::RepositoryService::Stub)
@ -139,7 +140,7 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
.with(expected_request, kind_of(Hash))
.and_return(double(value: true))
client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1)
client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false)
end
context 'SSH auth' do

View file

@ -95,4 +95,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_REMOVE_COMMENT_ACTION }
end
end
describe '.track_create_review_note_action' do
subject { described_class.track_create_review_note_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_CREATE_REVIEW_NOTE_ACTION }
end
end
describe '.track_publish_review_action' do
subject { described_class.track_publish_review_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION }
end
end
end

View file

@ -54,6 +54,18 @@ RSpec.describe Ci::Group do
.to be_a(Gitlab::Ci::Status::Failed)
end
end
context 'when one of the commit statuses in the group is allowed to fail' do
let(:jobs) do
[create(:ci_build, :failed, :allowed_to_fail),
create(:ci_build, :success)]
end
it 'fabricates a new detailed status object' do
expect(subject.detailed_status(double(:user)))
.to be_a(Gitlab::Ci::Status::SuccessWarning)
end
end
end
describe '.fabricate' do

View file

@ -5,12 +5,21 @@ require 'spec_helper'
RSpec.describe JiraService do
include AssetsHelpers
let_it_be(:project) { create(:project, :repository) }
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
let(:password) { 'jira-password' }
let(:transition_id) { 'test27' }
let(:server_info_results) { { 'deploymentType' => 'Cloud' } }
let(:jira_service) do
described_class.new(
project: project,
url: url,
username: username,
password: password
)
end
before do
WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json )
@ -19,7 +28,7 @@ RSpec.describe JiraService do
describe '#options' do
let(:options) do
{
project: create(:project),
project: project,
active: true,
username: 'username',
password: 'test',
@ -108,7 +117,7 @@ RSpec.describe JiraService do
describe '#create' do
let(:params) do
{
project: create(:project),
project: project,
url: url, api_url: api_url,
username: username, password: password,
jira_issue_transition_id: transition_id
@ -434,10 +443,23 @@ RSpec.describe JiraService do
end
end
describe '#find_issue' do
let(:issue_key) { 'JIRA-123' }
let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}" }
before do
stub_request(:get, issue_url).with(basic_auth: [username, password])
end
it 'call the Jira API to get the issue' do
jira_service.find_issue(issue_key)
expect(WebMock).to have_requested(:get, issue_url)
end
end
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
shared_examples 'close_issue' do
before do
@ -445,7 +467,6 @@ RSpec.describe JiraService do
allow(@jira_service).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
@ -657,17 +678,7 @@ RSpec.describe JiraService do
end
describe '#create_cross_reference_note' do
let_it_be(:user) { build_stubbed(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:jira_service) do
described_class.new(
project: project,
url: url,
username: username,
password: password
)
end
let_it_be(:user) { build_stubbed(:user) }
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
subject { jira_service.create_cross_reference_note(jira_issue, resource, user) }
@ -732,15 +743,6 @@ RSpec.describe JiraService do
describe '#test' do
let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } }
let_it_be(:project) { create(:project, :repository) }
let(:jira_service) do
described_class.new(
url: url,
project: project,
username: username,
password: password
)
end
def server_info
jira_service.test(nil)
@ -790,7 +792,6 @@ RSpec.describe JiraService do
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
project = create(:project)
service = project.create_jira_service(active: true)
expect(service.url).to eq('http://jira.sample/projects/project_a')

View file

@ -10,7 +10,7 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
describe '.execute' do
subject { service.execute }
let_it_be(:artifact, reload: true) do
let_it_be(:artifact, refind: true) do
create(:ci_job_artifact, expire_at: 1.day.ago)
end
@ -164,13 +164,21 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
end
context 'when timeout happens' do
let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) }
before do
stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_TIMEOUT', 1.second)
allow_any_instance_of(described_class).to receive(:destroy_pipeline_artifacts_batch) { true }
stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_TIMEOUT', 0.seconds)
stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1)
second_artifact.job.pipeline.unlocked!
end
it 'returns false and does not continue destroying' do
is_expected.to be_falsy
it 'destroys one artifact' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
end
it 'reports the number of destroyed artifacts' do
is_expected.to eq(1)
end
end
@ -187,6 +195,10 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
it 'destroys one artifact' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
end
it 'reports the number of destroyed artifacts' do
is_expected.to eq(1)
end
end
context 'when there are no artifacts' do
@ -197,6 +209,10 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
it 'does not raise error' do
expect { subject }.not_to raise_error
end
it 'reports the number of destroyed artifacts' do
is_expected.to eq(0)
end
end
context 'when there are artifacts more than batch sizes' do
@ -211,45 +227,9 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
it 'destroys all expired artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
end
end
context 'when artifact is a pipeline artifact' do
context 'when artifacts are expired' do
let!(:pipeline_artifact_1) { create(:ci_pipeline_artifact, expire_at: 1.week.ago) }
let!(:pipeline_artifact_2) { create(:ci_pipeline_artifact, expire_at: 1.week.ago) }
before do
[pipeline_artifact_1, pipeline_artifact_2].each { |pipeline_artifact| pipeline_artifact.pipeline.unlocked! }
stub_feature_flags(ci_split_pipeline_artifacts_removal: false)
end
it 'destroys pipeline artifacts' do
expect { subject }.to change { Ci::PipelineArtifact.count }.by(-2)
end
context 'with ci_split_pipeline_artifacts_removal enabled' do
before do
stub_feature_flags(ci_split_pipeline_artifacts_removal: true)
end
it 'does not destroy pipeline artifacts' do
expect { subject }.not_to change { Ci::PipelineArtifact.count }
end
end
end
context 'when artifacts are not expired' do
let!(:pipeline_artifact_1) { create(:ci_pipeline_artifact, expire_at: 2.days.from_now) }
let!(:pipeline_artifact_2) { create(:ci_pipeline_artifact, expire_at: 2.days.from_now) }
before do
[pipeline_artifact_1, pipeline_artifact_2].each { |pipeline_artifact| pipeline_artifact.pipeline.unlocked! }
end
it 'does not destroy pipeline artifacts' do
expect { subject }.not_to change { Ci::PipelineArtifact.count }
end
it 'reports the number of destroyed artifacts' do
is_expected.to eq(2)
end
end
@ -265,16 +245,4 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared
end
end
end
describe '.destroy_job_artifacts_batch' do
it 'returns a falsy value without artifacts' do
expect(service.send(:destroy_job_artifacts_batch)).to be_falsy
end
end
describe '.destroy_pipeline_artifacts_batch' do
it 'returns a falsy value without artifacts' do
expect(service.send(:destroy_pipeline_artifacts_batch)).to be_falsy
end
end
end

View file

@ -20,6 +20,23 @@ RSpec.describe DraftNotes::CreateService do
expect(draft.discussion_id).to be_nil
end
it 'tracks the start event when the draft is persisted' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_create_review_note_action)
.with(user: user)
draft = create_draft(note: 'This is a test')
expect(draft).to be_persisted
end
it 'does not track the start event when the draft is not persisted' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_create_review_note_action)
draft = create_draft(note: 'Not a reply!', resolve_discussion: true)
expect(draft).not_to be_persisted
end
it 'cannot resolve when there is nothing to resolve' do
draft = create_draft(note: 'Not a reply!', resolve_discussion: true)

View file

@ -43,6 +43,13 @@ RSpec.describe DraftNotes::PublishService do
expect(result[:status]).to eq(:success)
end
it 'does not track the publish event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_publish_review_action)
publish(draft: drafts.first)
end
context 'commit_id is set' do
let(:commit_id) { commit.id }
@ -74,6 +81,13 @@ RSpec.describe DraftNotes::PublishService do
expect { publish }.not_to change { DraftNote.count }
end
it 'does not track the publish event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_publish_review_action)
publish
end
it 'returns an error' do
result = publish
@ -105,6 +119,14 @@ RSpec.describe DraftNotes::PublishService do
publish
end
it 'tracks the publish event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_publish_review_action)
.with(user: user)
publish
end
context 'commit_id is set' do
let(:commit_id) { commit.id }

View file

@ -68,6 +68,12 @@ RSpec.shared_examples 'boards listable model' do |list_factory|
expect(subject.title).to eq 'Development'
end
it 'returns Open when list_type is set to backlog' do
subject.list_type = :backlog
expect(subject.title).to eq 'Open'
end
it 'returns Closed when list_type is set to closed' do
subject.list_type = :closed

View file

@ -8,9 +8,11 @@ RSpec.describe ExpireBuildArtifactsWorker do
describe '#perform' do
it 'executes a service' do
expect_next_instance_of(Ci::DestroyExpiredJobArtifactsService) do |instance|
expect(instance).to receive(:execute)
expect(instance).to receive(:execute).and_call_original
end
expect(worker).to receive(:log_extra_metadata_on_done).with(:destroyed_job_artifacts_count, 0)
worker.perform
end
end