Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
18873553de
commit
8106ac487c
95 changed files with 1029 additions and 353 deletions
|
@ -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
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -1 +1 @@
|
|||
a6674b359a02a4bf0549dcaa77ac05b1f4850831
|
||||
3083074640633df94cbeee611795a6fc6d8c5607
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
5
app/assets/javascripts/api/api_utils.js
Normal file
5
app/assets/javascripts/api/api_utils.js
Normal 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));
|
||||
}
|
1
app/assets/javascripts/api/constants.js
Normal file
1
app/assets/javascripts/api/constants.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const DEFAULT_PER_PAGE = 20;
|
22
app/assets/javascripts/api/groups_api.js
Normal file
22
app/assets/javascripts/api/groups_api.js
Normal 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;
|
||||
});
|
||||
}
|
27
app/assets/javascripts/api/projects_api.js
Normal file
27
app/assets/javascripts/api/projects_api.js
Normal 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 };
|
||||
});
|
||||
}
|
66
app/assets/javascripts/api/user_api.js
Normal file
66
app/assets/javascripts/api/user_api.js
Normal 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,
|
||||
});
|
||||
}
|
|
@ -33,13 +33,13 @@ export default {
|
|||
GlDropdownText,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
inject: ['groupId'],
|
||||
props: {
|
||||
list: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
inject: ['groupId'],
|
||||
data() {
|
||||
return {
|
||||
initialLoading: true,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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] = {};
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
15
app/assets/javascripts/rest_api.js
Normal file
15
app/assets/javascripts/rest_api.js
Normal 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')
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.')
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Correct status indicator for jobs groups when failure is allowed
|
||||
merge_request: 51478
|
||||
author: Sune Keller (sirlatrom)
|
||||
type: fixed
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix multiple errors in custom server hook render improperly
|
||||
merge_request: 51001
|
||||
author: Kev @KevSlashNull
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add metrics to starting and publishing a review
|
||||
merge_request: 51521
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Flash transfer errors in the admin project controller
|
||||
merge_request: 50541
|
||||
author: Vincent Fazio
|
||||
type: fixed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Extract expired pipeline artifacts removal service into it's own background
|
||||
worker
|
||||
merge_request: 51323
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Default enable set_user_availability_status
|
||||
merge_request: 51668
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add documentation for new Snippet repository storage move API
|
||||
merge_request: 50151
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed applied message showing before discussion gets resolved
|
||||
merge_request: 51605
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rename Backlog list to Open in issue boards
|
||||
merge_request: 51562
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/terraform-0-14-cache.yml
Normal file
5
changelogs/unreleased/terraform-0-14-cache.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update Terraform Pipline templaes to support 0.14 lockfile cache
|
||||
merge_request: 50647
|
||||
author: Aurelian Shuttleworth
|
||||
type: fixed
|
5
changelogs/unreleased/yo-master-patch-32789.yml
Normal file
5
changelogs/unreleased/yo-master-patch-32789.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix slack application helper card
|
||||
merge_request: 51034
|
||||
author: Yogi (@yo)
|
||||
type: fixed
|
5
changelogs/unreleased/yo-master-patch-66129.yml
Normal file
5
changelogs/unreleased/yo-master-patch-66129.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove margin top for snippets empty state
|
||||
merge_request: 51038
|
||||
author: Yogi (@yo)
|
||||
type: fixed
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -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
|
||||
|
|
293
doc/api/snippet_repository_storage_moves.md
Normal file
293
doc/api/snippet_repository_storage_moves.md
Normal 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"
|
||||
}
|
||||
```
|
|
@ -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)
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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)** |
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -8,6 +8,10 @@ module Gitlab
|
|||
def self.common_helpers
|
||||
Status::Group::Common
|
||||
end
|
||||
|
||||
def self.extended_statuses
|
||||
[[Status::SuccessWarning]]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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)}'"
|
||||
|
|
|
@ -19,6 +19,7 @@ cache:
|
|||
key: "${TF_ROOT}"
|
||||
paths:
|
||||
- ${TF_ROOT}/.terraform/
|
||||
- ${TF_ROOT}/.terraform.lock.hcl
|
||||
|
||||
.init: &init
|
||||
stage: init
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
35
spec/frontend/api/api_utils_spec.js
Normal file
35
spec/frontend/api/api_utils_spec.js
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue