Commit afa418e5 authored by Pawel Chojnacki's avatar Pawel Chojnacki

Merge remote-tracking branch 'upstream/master' into 5029-support-cluster-metrics

parents 3243cdc4 8b0345d7
Please view this file on the master branch, on stable branches it's out of date.
## 10.5.2 (2018-02-25)
- No changes.
## 10.5.1 (2018-02-22)
- No changes.
......
......@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.5.2 (2018-02-25)
### Fixed (7 changes)
- Fix single digit value clipping for stacked progress bar. !17217
- Fix issue with cache key being empty when variable used as the key. !17260
- Enable Legacy Authorization by default on Cluster creations. !17302
- Allow branch names to be named the same as the sha it points to.
- Fix 500 error when loading an invalid upload URL.
- Don't attempt to update user tracked fields if database is in read-only.
- Prevent MR Widget error when no CI configured.
### Performance (5 changes)
- Improve query performance for snippets dashboard. !17088
- Only check LFS integrity for first ref in a push to avoid timeout. !17098
- Improve query performance of MembersFinder. !17190
- Increase feature flag cache TTL to one hour.
- Improve performance of searching for and autocompleting of users.
## 10.5.1 (2018-02-22)
- No changes.
......
......@@ -30,7 +30,7 @@ import collapseIcon from './icons/fullscreen_collapse.svg';
import expandIcon from './icons/fullscreen_expand.svg';
import tooltip from '../vue_shared/directives/tooltip';
$(() => {
export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
......@@ -366,4 +366,4 @@ $(() => {
'boards-selector': gl.issueBoards.BoardsSelector,
}
});
});
};
<script>
import eventHub from '../event_hub';
import FilteredSearchTokenizer from '../filtered_search_tokenizer';
export default {
name: 'RecentSearchesDropdownContent',
props: {
items: {
type: Array,
......@@ -19,7 +19,6 @@ export default {
required: true,
},
},
computed: {
processedItems() {
return this.items.map((item) => {
......@@ -42,7 +41,6 @@ export default {
return this.items.length > 0;
},
},
methods: {
onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text);
......@@ -54,8 +52,9 @@ export default {
eventHub.$emit('requestClearRecentSearches');
},
},
template: `
};
</script>
<template>
<div>
<div
v-if="!isLocalStorageAvailable"
......@@ -65,16 +64,20 @@ export default {
<ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="index">
:key="`processed-items-${index}`"
>
<button
type="button"
class="filtered-search-history-dropdown-item"
@click="onItemActivated(item.text)">
<span>
<span
v-for="(token, tokenIndex) in item.tokens"
class="filtered-search-history-dropdown-token">
<span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span>
class="filtered-search-history-dropdown-token"
v-for="(token, index) in item.tokens"
:key="`dropdown-token-${index}`"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
</span>
</span>
<span class="filtered-search-history-dropdown-search-token">
......@@ -98,5 +101,4 @@ export default {
You don't have any recent searches
</div>
</div>
`,
};
</template>
import Vue from 'vue';
import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content';
import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue';
import eventHub from './event_hub';
class RecentSearchesRoot {
......@@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({
el: this.wrapperElement,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
RecentSearchesDropdownContent,
},
data() { return state; },
template: `
......
<script>
import { mapState } from 'vuex';
import { sprintf, __ } from '../../../locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
export default {
components: {
RadioGroup,
},
computed: {
...mapState([
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` },
false,
);
},
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
</script>
<template>
<div class="append-bottom-15 ide-commit-radios">
<radio-group
:value="$options.commitToCurrentBranch"
:checked="true"
>
<span
v-html="commitToCurrentBranchText"
>
</span>
</radio-group>
<radio-group
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
:help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:help-text="newMergeRequestHelpText"
/>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: false,
default: null,
},
checked: {
type: Boolean,
required: false,
default: false,
},
showInput: {
type: Boolean,
required: false,
default: false,
},
helpText: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapState('commit', [
'commitAction',
]),
...mapGetters('commit', [
'newBranchName',
]),
},
methods: {
...mapActions('commit', [
'updateCommitAction',
'updateBranchName',
]),
},
};
</script>
<template>
<fieldset>
<label>
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
:checked="checked"
v-once
/>
<span class="prepend-left-10">
<template v-if="label">
{{ label }}
</template>
<slot v-else></slot>
<span
v-if="helpText"
v-tooltip
class="help-block inline"
:title="helpText"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
</span>
</span>
</label>
<div
v-if="commitAction === value && showInput"
class="ide-commit-new-branch"
>
<input
type="text"
class="form-control"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
</div>
</fieldset>
</template>
......@@ -22,7 +22,7 @@
},
data() {
return {
width: 290,
width: 340,
};
},
computed: {
......@@ -120,7 +120,7 @@
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="290"
:start-size="340"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
......
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapActions, mapGetters } from 'vuex';
import * as consts from '../stores/modules/commit/constants';
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
import modal from '../../vue_shared/components/modal.vue';
import commitFilesList from './commit_sidebar/list.vue';
import Actions from './commit_sidebar/actions.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
modal,
icon,
commitFilesList,
Actions,
LoadingButton,
},
directives: {
tooltip,
......@@ -24,14 +29,6 @@ export default {
required: true,
},
},
data() {
return {
showNewBranchModal: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: {
...mapState([
'currentProjectId',
......@@ -40,74 +37,40 @@ export default {
'lastCommitMsg',
'changedFiles',
]),
commitButtonDisabled() {
return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
},
commitMessageCount() {
return this.commitMessage.length;
},
...mapState('commit', [
'commitMessage',
'submitCommitLoading',
]),
...mapGetters('commit', [
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions([
'checkCommitStatus',
'commitChanges',
'getTreeData',
'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
branch: createNewBranch ?
`${this.currentBranchId}-${new Date().getTime().toString()}` :
this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranchId : undefined,
};
this.showNewBranchModal = false;
this.submitCommitsLoading = true;
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.commitMessage = '';
this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
tryCommit() {
this.submitCommitsLoading = true;
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
this.showNewBranchModal = true;
} else {
this.makeCommit();
}
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH)
.then(() => this.commitChanges());
},
},
};
</script>
......@@ -120,15 +83,17 @@ export default {
}"
>
<modal
v-if="showNewBranchModal"
id="ide-create-branch-modal"
:primary-button-label="__('Create new branch')"
kind="primary"
kind="success"
:title="__('Branch has changed')"
:text="__(`This branch has changed since
you started editing. Would you like to create a new branch?`)"
@cancel="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
@submit="forceCreateNewBranch"
>
<template slot="body">
{{ __(`This branch has changed since you started editing.
Would you like to create a new branch?`) }}
</template>
</modal>
<commit-files-list
title="Staged"
:file-list="changedFiles"
......@@ -140,51 +105,36 @@ you started editing. Would you like to create a new branch?`)"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message ref-name"
class="form-control multi-file-commit-message"
name="commit-message"
v-model="commitMessage"
placeholder="Commit message"
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<div class="multi-file-commit-fieldset">
<label
v-tooltip
title="Create a new merge request with these changes"
data-container="body"
data-placement="top"
>
<input
type="checkbox"
v-model="startNewMR"
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
{{ __('Merge Request') }}
</label>
<button
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
<i
v-if="submitCommitsLoading"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading"
>
</i>
{{ __('Commit') }}
{{ __('Discard draft') }}
</button>
<div
class="multi-file-commit-message-count"
>
{{ commitMessageCount }}
</div>
</div>
</form>
</template>
......@@ -216,8 +166,7 @@ you started editing. Would you like to create a new branch?`)"
<h4>
{{ __('All changes are committed') }}
</h4>
<p>
{{ lastCommitMsg }}
<p v-html="lastCommitMsg">
</p>
</div>
</div>
......
import Vue from 'vue';
export default new Vue();
/* global monaco */
import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
constructor(monaco, file) {
......@@ -22,6 +23,9 @@ export default class Model {
);
this.events = new Map();
this.updateContent = this.updateContent.bind(this);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
......@@ -61,8 +65,15 @@ export default class Model {
);
}
updateContent(content) {
this.getModel().setValue(content);
this.getOriginalModel().setValue(content);
}
dispose() {
this.disposable.dispose();
this.events.clear();
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -51,84 +48,6 @@ export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
export const checkCommitStatus = ({ state }) =>
service
.getBranchData(state.currentProjectId, state.currentBranchId)
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
state.projects[state.currentProjectId].branches[state.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = (
{ commit, state, dispatch },
{ payload, newMr },
) =>
service
.commit(state.currentProjectId, payload)
.then(({ data }) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
return;
}
const selectedProject = state.projects[state.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
message: data.message,
authored_date: data.committed_date,
},
};
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
commit(types.SET_LAST_COMMIT_MSG, commitMsg);
if (newMr) {
dispatch('discardAllChanges');
dispatch(
'redirectToUrl',
`${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
projectId: state.currentProjectId,
branchId: state.currentBranchId,
reference: data.id,
});
state.changedFiles.forEach((entry) => {
commit(types.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
});
});
dispatch('discardAllChanges');
window.scrollTo(0, 0);
}
})
.catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
......
......@@ -4,6 +4,7 @@ import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
Vue.use(Vuex);
......@@ -12,4 +13,7 @@ export default new Vuex.Store({
actions,
mutations,
getters,
modules: {
commit: commitModule,
},
});
import { sprintf, __ } from '../../../../locale';
import * as types from './mutation_types';
import * as consts from './constants';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
import service from '../../../services';
import flash from '../../../../flash';
import { stripHtml } from '../../../../lib/utils/text_utility';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
};
export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
export const updateCommitAction = ({ commit }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, commitAction);
};
export const updateBranchName = ({ commit }, branchName) => {
commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
};
export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats ?
sprintf(
__('with %{additions} additions, %{deletions} deletions.'),
{ additions: data.stats.additions, deletions: data.stats.deletions },
)
: '';
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
{
commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${data.short_id}</a>`,
commitStats,
},
false,
);
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
};
export const checkCommitStatus = ({ rootState }) =>
service
.getBranchData(rootState.currentProjectId, rootState.currentBranchId)
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
}
return false;
})
.catch(() => flash(__('Error checking branch data. Please try again.'), 'alert', document, null, false, true));
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
{ data, branch },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
id: data.id,
message: data.message,
authored_date: data.committed_date,
author_name: data.committer_name,
},
};
commit(rootTypes.SET_BRANCH_WORKING_REFERENCE, {
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
reference: data.id,
}, { root: true });
rootState.changedFiles.forEach((entry) => {
commit(rootTypes.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
}, { root: true });
commit(rootTypes.SET_FILE_RAW_DATA, {
file: entry,
raw: entry.content,
}, { root: true });
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.raw);
});
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
router.push(`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`);
}
dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
};
export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
commit(types.UPDATE_LOADING, true);
return getCommitStatus.then(branchChanged => new Promise((resolve) => {
if (branchChanged) {
// show the modal with a Bootstrap call
$('#ide-create-branch-modal').modal('show');
} else {
resolve();
}
}))
.then(() => service.commit(rootState.currentProjectId, payload))
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
return;
}
dispatch('setLastCommitMessage', data);
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
'redirectToUrl',
createNewMergeRequestUrl(
rootState.projects[rootState.currentProjectId].web_url,
getters.branchName,
rootState.currentBranchId,
),
{ root: true },
);
} else {
dispatch('updateFilesAfterCommit', { data, branch: getters.branchName });
}
})
.catch((err) => {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false);
});
};
export const COMMIT_TO_CURRENT_BRANCH = '1';
export const COMMIT_TO_NEW_BRANCH = '2';
export const COMMIT_TO_NEW_BRANCH_MR = '3';
import * as consts from './constants';
export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
export const branchName = (state, getters, rootState) => {
if (
state.commitAction === consts.COMMIT_TO_NEW_BRANCH ||
state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR
) {
if (state.newBranchName === '') {
return getters.newBranchName;
}
return state.newBranchName;
}
return rootState.currentBranchId;
};
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
export default {
namespaced: true,
state: state(),
mutations,
actions,
getters,
};
export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE';
export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
import * as types from './mutation_types';
export default {
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
Object.assign(state, {
commitMessage,
});
},
[types.UPDATE_COMMIT_ACTION](state, commitAction) {
Object.assign(state, {
commitAction,
});
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
Object.assign(state, {
newBranchName,
});
},
[types.UPDATE_LOADING](state, submitCommitLoading) {
Object.assign(state, {
submitCommitLoading,
});
},
};
export default () => ({
commitMessage: '',
commitAction: '1',
newBranchName: '',
submitCommitLoading: false,
});
......@@ -187,3 +187,18 @@ export const createOrMergeEntry = ({ projectId,
level,
});
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
actions: rootState.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: newBranch ? rootState.currentBranchId : undefined,
});
export const createNewMergeRequestUrl = (projectUrl, source, target) =>
`${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`;
......@@ -123,9 +123,10 @@ export default {
and cause odd behavior when one is removed.
-->
<li
:key="`${pendingReferences.length}-${reference}`"
v-for="(reference, index) in pendingReferences"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item">
:key="`related-issues-token-${index}`"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item"
>
<issue-token
event-namespace="pendingIssuable"
:id-key="index"
......
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
});
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
});
......@@ -3,6 +3,7 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new
......
......@@ -231,7 +231,8 @@ export default {
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button">
type="button"
class="qa-merge-button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
......
......@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children"
>
<button
type="button"
class="btn btn-sm btn-reopen btn-success"
class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
:disabled="isMakingRequest"
@click="rebase"
>
......
......@@ -13,6 +13,16 @@
display: inline-block;
}
.issuable-meta {
.author_link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
......
......@@ -19,6 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 40px;
color: $almost-black;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
......@@ -265,7 +266,7 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
width: 290px;
width: 340px;
padding: 0;
background-color: $gray-light;
padding-right: 3px;
......@@ -465,20 +466,14 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-form {
padding: $gl-padding;
border-top: 1px solid $white-dark;
}
.multi-file-commit-fieldset {
display: flex;
align-items: center;
padding-bottom: 12px;
.btn {
flex: 1;
font-size: $gl-font-size;
}
}
.multi-file-commit-message.form-control {
height: 80px;
height: 160px;
resize: none;
}
......@@ -639,3 +634,18 @@ table.table tr td.multi-file-table-name {
left: 0;
}
}
.ide-commit-radios {
label {
font-weight: normal;
}
.help-block {
margin-top: 0;
line-height: 0;
}
}
.ide-commit-new-branch {
margin-left: 25px;
}
......@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy]
before_action :domain, only: [:show, :destroy, :verify]
def show
end
......@@ -12,11 +12,23 @@ class Projects::PagesDomainsController < Projects::ApplicationController
@domain = @project.pages_domains.new
end
def verify
result = VerifyPagesDomainService.new(@domain).execute
if result[:status] == :success
flash[:notice] = 'Successfully verified domain ownership'
else
flash[:alert] = 'Failed to verify domain ownership'
end
redirect_to project_pages_domain_path(@project, @domain)
end
def create
@domain = @project.pages_domains.create(pages_domain_params)
if @domain.valid?
redirect_to project_pages_path(@project)
redirect_to project_pages_domain_path(@project, @domain)
else
render 'new'
end
......@@ -46,6 +58,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def domain
@domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
@domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s)
end
end
......@@ -104,8 +104,7 @@ class ProjectsController < Projects::ApplicationController
end
def show
# If we're importing while we do have a repository, we're simply updating the mirror.
if @project.import_in_progress? && !@project.updating_mirror?
if @project.import_in_progress?
redirect_to project_import_path(@project)
return
end
......
......@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end
end
elsif only_group_labels?
label_ids << Label.where(group_id: group.id)
label_ids << Label.where(group_id: group_ids)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
......@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
def group
strong_memoize(:group) do
def group_ids
strong_memoize(:group_ids) do
group = Group.find(params[:group_id])
authorized_to_read_labels?(group) && group
groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
groups_user_can_read_labels(groups).map(&:id)
end
end
......@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent)
end
def groups_user_can_read_labels(groups)
DeclarativePolicy.user_scope do
groups.select { |group| authorized_to_read_labels?(group) }
end
end
end
......@@ -200,6 +200,7 @@ module ApplicationSettingsHelper
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
:pages_domain_verification_enabled,
:password_authentication_enabled_for_web,
:password_authentication_enabled_for_git,
:performance_bar_allowed_group_id,
......
......@@ -16,71 +16,38 @@ module BlobHelper
options[:link_opts])
end
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob && blob.readable_text?
common_classes = "btn js-edit-blob #{options[:extra_class]}"
if !on_top_of_branch?(project, ref)
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag 'Edit',
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: 'edit', fork_path: fork_path }
end
end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
end
def ide_edit_text
"#{_('Web IDE')}"
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
return unless blob = readable_blob(options, path, project, ref)
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless show_new_ide?(project)
common_classes = "btn js-edit-blob #{options[:extra_class]}"
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
edit_button_tag(blob,
common_classes,
_('Edit'),
edit_blob_path(project, ref, path, options),
project,
ref)
end
return unless blob && blob.readable_text?
def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
return unless show_new_ide?(project)
return unless blob = readable_blob(options, path, project, ref)
common_classes = "btn js-edit-ide #{options[:extra_class]}"
if !on_top_of_branch?(project, ref)
button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif current_user && can_modify_blob?(blob, project, ref)
link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
to: ide_edit_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag ide_edit_text,
class: common_classes,
data: { fork_path: fork_path }
end
edit_button_tag(blob,
common_classes,
_('Web IDE'),
ide_edit_path(project, ref, path, options),
project,
ref)
end
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
......@@ -96,21 +63,12 @@ module BlobHelper
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
modify_file_button(
project,
ref,
path,
......@@ -122,7 +80,7 @@ module BlobHelper
end
def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
modify_file_button(
project,
ref,
path,
......@@ -332,4 +290,55 @@ module BlobHelper
options
end
def readable_blob(options, path, project, ref)
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
blob if blob&.readable_text?
end
def edit_blob_fork_params(path)
{
to: path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
end
def edit_modify_file_fork_params(action)
{
to: request.fullpath,
notice: edit_in_new_fork_notice_action(action),
notice_now: edit_in_new_fork_notice_now
}
end
def edit_fork_button_tag(common_classes, project, label, params, action = 'edit')
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params)
button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
end
def edit_disabled_button_tag(button_text, common_classes)
button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
end
def edit_link_tag(link_text, edit_path, common_classes)
link_to link_text, edit_path, class: "#{common_classes} btn-sm"
end
def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in
# Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
elsif current_user && can?(current_user, :fork_project, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
end
end
......@@ -237,7 +237,7 @@ module IssuablesHelper
data.merge!(updated_at_by(issuable))
data.to_json
data
end
def updated_at_by(issuable)
......
......@@ -83,6 +83,10 @@ module TreeHelper
" A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
def edit_in_new_fork_notice_action(action)
edit_in_new_fork_notice + " Try to #{action} this file again."
end
def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started."
end
......
module Emails
module PagesDomains
def pages_domain_enabled_email(domain, recipient)
@domain = domain
@project = domain.project
mail(
to: recipient.notification_email,
subject: subject("GitLab Pages domain '#{domain.domain}' has been enabled")
)
end
def pages_domain_disabled_email(domain, recipient)
@domain = domain
@project = domain.project
mail(
to: recipient.notification_email,
subject: subject("GitLab Pages domain '#{domain.domain}' has been disabled")
)
end
def pages_domain_verification_succeeded_email(domain, recipient)
@domain = domain
@project = domain.project
mail(
to: recipient.notification_email,
subject: subject("Verification succeeded for GitLab Pages domain '#{domain.domain}'")
)
end
def pages_domain_verification_failed_email(domain, recipient)
@domain = domain
@project = domain.project
mail(
to: recipient.notification_email,
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
)
end
end
end
......@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::CsvExport
include Emails::MergeRequests
include Emails::Notes
include Emails::PagesDomains
include Emails::Projects
include Emails::Profile
include Emails::Pipelines
......
class PagesDomain < ActiveRecord::Base
VERIFICATION_KEY = 'gitlab-pages-verification-code'.freeze
VERIFICATION_THRESHOLD = 3.days.freeze
belongs_to :project
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
......@@ -16,10 +20,32 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
after_initialize :set_verification_code
after_create :update_daemon
after_save :update_daemon
after_update :update_daemon, if: :pages_config_changed?
after_destroy :update_daemon
scope :enabled, -> { where('enabled_until >= ?', Time.now ) }
scope :needs_verification, -> do
verified_at = arel_table[:verified_at]
enabled_until = arel_table[:enabled_until]
threshold = Time.now + VERIFICATION_THRESHOLD
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
def verified?
!!verified_at
end
def unverified?
!verified?
end
def enabled?
!Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present?
end
def to_param
domain
end
......@@ -84,12 +110,49 @@ class PagesDomain < ActiveRecord::Base
@certificate_text ||= x509.try(:to_text)
end
# Verification codes may be TXT records for domain or verification_domain, to
# support the use of CNAME records on domain.
def verification_domain
return unless domain.present?
"_#{VERIFICATION_KEY}.#{domain}"
end
def keyed_verification_code
return unless verification_code.present?
"#{VERIFICATION_KEY}=#{verification_code}"
end
private
def set_verification_code
return if self.verification_code.present?
self.verification_code = SecureRandom.hex(16)
end
def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute
end
def pages_config_changed?
project_id_changed? ||
domain_changed? ||
certificate_changed? ||
key_changed? ||
became_enabled? ||
became_disabled?
end
def became_enabled?
enabled_until.present? && !enabled_until_was.present?
end
def became_disabled?
!enabled_until.present? && enabled_until_was.present?
end
def validate_matching_key
unless has_matching_key?
self.errors.add(:key, "doesn't match the certificate")
......
......@@ -4,13 +4,33 @@ module Ci
return if job.job_artifacts_trace
job.trace.read do |stream|
if stream.file?
break unless stream.file?
clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_job_trace!(job, clone_path)
FileUtils.rm(stream.path)
end
end
end
private
def create_job_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
def clone_file!(src_path, temp_dir)
FileUtils.mkdir_p(temp_dir)
Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.copy(src_path, temp_path)
yield(temp_path)
end
end
end
end
......@@ -79,8 +79,12 @@ class IssuableBaseService < BaseService
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label = Labels::FindOrCreateService.new(
current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id)
end.compact
......@@ -104,7 +108,7 @@ class IssuableBaseService < BaseService
end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_quick_actions_into_params!(issuable)
......@@ -305,4 +309,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
def parent
project
end
end
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
def initialize(current_user, parent, params = {})
@current_user = current_user
@project = project
@parent = parent
@available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access
end
......@@ -13,12 +14,13 @@ module Labels
private
attr_reader :current_user, :project, :params, :skip_authorization
attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels
@available_labels ||= LabelsFinder.new(
current_user,
project_id: project.id
"#{parent_type}_id".to_sym => parent.id,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
......@@ -27,8 +29,8 @@ module Labels
def find_or_create_label
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
new_label = Labels::CreateService.new(params).execute(project: project)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end
new_label
......@@ -37,5 +39,13 @@ module Labels
def title
params[:title] || params[:name]
end
def parent_type
parent.model_name.param_key
end
def parent_is_group?
parent_type == "group"
end
end
end
......@@ -362,6 +362,30 @@ class NotificationService
end
end
def pages_domain_verification_succeeded(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
end
end
def pages_domain_verification_failed(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_failed_email(domain, user).deliver_later
end
end
def pages_domain_enabled(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_enabled_email(domain, user).deliver_later
end
end
def pages_domain_disabled(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_disabled_email(domain, user).deliver_later
end
end
protected
def new_resource_email(target, method)
......@@ -480,6 +504,14 @@ class NotificationService
private
def recipients_for_pages_domain(domain)
project = domain.project
return [] unless project
notifiable_users(project.team.masters, :watch, target: project)
end
def notifiable?(*args)
NotificationRecipientService.notifiable?(*args)
end
......
......@@ -50,16 +50,7 @@ module Projects
return [] unless noteable&.is_a?(Issuable)
opts = {
project: project,
issuable: noteable,
current_user: current_user
}
QuickActions::InterpretService.command_definitions.map do |definition|
next unless definition.available?(opts)
definition.to_h(opts)
end.compact
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end
end
end
......@@ -23,7 +23,7 @@ module Projects
end
def pages_domains_config
project.pages_domains.map do |domain|
enabled_pages_domains.map do |domain|
{
domain: domain.domain,
certificate: domain.certificate,
......@@ -32,6 +32,14 @@ module Projects
end
end
def enabled_pages_domains
if Gitlab::CurrentSettings.pages_domain_verification_enabled?
project.pages_domains.enabled
else
project.pages_domains
end
end
def reload_daemon
# GitLab Pages daemon constantly watches for modification time of `pages.path`
# It reloads configuration when `pages.path` is modified
......
......@@ -8,6 +8,18 @@ module QuickActions
SHRUG = '¯\\_(ツ)_/¯'.freeze
TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze
# Takes an issuable and returns an array of all the available commands
# represented with .to_h
def available_commands(issuable)
@issuable = issuable
self.class.command_definitions.map do |definition|
next unless definition.available?(self)
definition.to_h(self)
end.compact
end
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable)
......@@ -16,8 +28,8 @@ module QuickActions
@issuable = issuable
@updates = {}
content, commands = extractor.extract_commands(content, context)
extract_updates(commands, context)
content, commands = extractor.extract_commands(content)
extract_updates(commands)
[content, @updates]
end
......@@ -29,8 +41,8 @@ module QuickActions
@issuable = issuable
content, commands = extractor.extract_commands(content, context)
commands = explain_commands(commands, context)
content, commands = extractor.extract_commands(content)
commands = explain_commands(commands)
[content, commands]
end
......@@ -158,11 +170,11 @@ module QuickActions
params '%"milestone"'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.milestones.active.any?
find_milestones(project, state: 'active').any?
end
parse_params do |milestone_param|
extract_references(milestone_param, :milestone).first ||
project.milestones.find_by(title: milestone_param.strip)
find_milestones(project, title: milestone_param.strip).first
end
command :milestone do |milestone|
@updates[:milestone_id] = milestone.id if milestone
......@@ -545,6 +557,10 @@ module QuickActions
users
end
def find_milestones(project, params = {})
MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute
end
def find_labels(labels_param)
extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
......@@ -558,21 +574,21 @@ module QuickActions
find_labels(labels_param).map(&:id)
end
def explain_commands(commands, opts)
def explain_commands(commands)
commands.map do |name, arg|
definition = self.class.definition_by_name(name)
next unless definition
definition.explain(self, opts, arg)
definition.explain(self, arg)
end.compact
end
def extract_updates(commands, opts)
def extract_updates(commands)
commands.each do |name, arg|
definition = self.class.definition_by_name(name)
next unless definition
definition.execute(self, opts, arg)
definition.execute(self, arg)
end
end
......@@ -582,14 +598,5 @@ module QuickActions
ext.references(type)
end
def context
{
issuable: issuable,
current_user: current_user,
project: project,
params: params
}
end
end
end
require 'resolv'
class VerifyPagesDomainService < BaseService
# The maximum number of seconds to be spent on each DNS lookup
RESOLVER_TIMEOUT_SECONDS = 15
# How long verification lasts for
VERIFICATION_PERIOD = 7.days
attr_reader :domain
def initialize(domain)
@domain = domain
end
def execute
return error("No verification code set for #{domain.domain}") unless domain.verification_code.present?
if !verification_enabled? || dns_record_present?
verify_domain!
elsif expired?
disable_domain!
else
unverify_domain!
end
end
private
def verify_domain!
was_disabled = !domain.enabled?
was_unverified = domain.unverified?
# Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
domain.update!(verified_at: Time.now, enabled_until: reverify)
if was_disabled
notify(:enabled)
elsif was_unverified
notify(:verification_succeeded)
end
success
end
def unverify_domain!
if domain.verified?
domain.update!(verified_at: nil)
notify(:verification_failed)
end
error("Couldn't verify #{domain.domain}")
end
def disable_domain!
domain.update!(verified_at: nil, enabled_until: nil)
notify(:disabled)
error("Couldn't verify #{domain.domain}. It is now disabled.")
end
# A domain is only expired until `disable!` has been called
def expired?
domain.enabled_until && domain.enabled_until < Time.now
end
def dns_record_present?
Resolv::DNS.open do |resolver|
resolver.timeouts = RESOLVER_TIMEOUT_SECONDS
check(domain.domain, resolver) || check(domain.verification_domain, resolver)
end
end
def check(domain_name, resolver)
records = parse(txt_records(domain_name, resolver))
records.any? do |record|
record == domain.keyed_verification_code || record == domain.verification_code
end
rescue => err
log_error("Failed to check TXT records on #{domain_name} for #{domain.domain}: #{err}")
false
end
def txt_records(domain_name, resolver)
resolver.getresources(domain_name, Resolv::DNS::Resource::IN::TXT)
end
def parse(records)
records.flat_map(&:strings).flat_map(&:split)
end
def verification_enabled?
Gitlab::CurrentSettings.pages_domain_verification_enabled?
end
def notify(type)
return unless verification_enabled?
Gitlab::AppLogger.info("Pages domain '#{domain.domain}' changed state to '#{type}'")
notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -272,6 +272,17 @@
.col-sm-10
= f.number_field :max_pages_size, class: 'form-control'
.help-block 0 for unlimited
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :pages_domain_verification_enabled do
= f.check_box :pages_domain_verification_enabled
Require users to prove ownership of custom domains
.help-block
Domain verification is an essential security measure for public GitLab
sites. Users are required to demonstrate they control a domain before
it is enabled
= link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
%fieldset
%legend Continuous Integration and Deployment
......
%p
Following a verification check, your GitLab Pages custom domain has been
%strong disabled.
This means that your content is no longer visible at #{link_to @domain.url, @domain.url}
%p
Project: #{link_to @project.human_name, project_url(@project)}
%p
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
If this domain has been disabled in error, please follow
= link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
to verify and re-enable your domain.
%p
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
Following a verification check, your GitLab Pages custom domain has been
**disabled**. This means that your content is no longer visible at #{@domain.url}
Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
If this domain has been disabled in error, please follow these instructions
to verify and re-enable your domain:
= help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
%p
Following a verification check, your GitLab Pages custom domain has been
enabled. You should now be able to view your content at #{link_to @domain.url, @domain.url}
%p
Project: #{link_to @project.human_name, project_url(@project)}
%p
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
Please visit
= link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
Following a verification check, your GitLab Pages custom domain has been
enabled. You should now be able to view your content at #{@domain.url}
Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
Please visit
= help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
%p
Verification has failed for one of your GitLab Pages custom domains!
%p
Project: #{link_to @project.human_name, project_url(@project)}
%p
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
Unless you take action, it will be disabled on
%strong= @domain.enabled_until.strftime('%F %T.')
Until then, you can view your content at #{link_to @domain.url, @domain.url}
%p
Please visit
= link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
%p
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
Verification has failed for one of your GitLab Pages custom domains!
Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
Unless you take action, it will be disabled on *#{@domain.enabled_until.strftime('%F %T')}*.
Until then, you can view your content at #{@domain.url}
Please visit
= help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
%p
One of your GitLab Pages custom domains has been successfully verified!
%p
Project: #{link_to @project.human_name, project_url(@project)}
%p
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
This is a notification. No action is required on your part. You can view your
content at #{link_to @domain.url, @domain.url}
%p
Please visit
= link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
One of your GitLab Pages custom domains has been successfully verified!
Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
No action is required on your part. You can view your content at #{@domain.url}
Please visit
= help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
for more information about custom domain verification.
......@@ -3,7 +3,7 @@
.radio
= label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff"
%strong Fast-forward merge
%br
%span.descr
......
......@@ -13,8 +13,8 @@
.btn-group{ role: "group" }<
= lock_file_link(html_options: { class: 'btn btn-sm path-lock' })
= edit_blob_link
= ide_blob_link
= edit_blob_button
= ide_edit_button
- if current_user
= replace_blob_link
= delete_blob_link
......
......@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
- content_for :page_specific_javascripts do
= webpack_bundle_tag('blob')
......@@ -17,7 +17,7 @@
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
= edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- if image_diff && image_replaced
......
......@@ -98,7 +98,7 @@
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'projects/ee/merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
= f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes"
= render 'projects/ee/service_desk_settings'
......
......@@ -58,7 +58,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue)
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
......@@ -95,6 +95,3 @@
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issue_show')
......@@ -3,15 +3,26 @@
.panel-heading
Domains (#{@domains.count})
%ul.well-list
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- @domains.each do |domain|
%li
.pull-right
= link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped"
= link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
.clearfix
%span= link_to domain.domain, domain.url
- if verification_enabled
- tooltip, status = domain.unverified? ? ['Unverified', 'failed'] : ['Verified', 'success']
= link_to domain.url, title: tooltip, class: 'has-tooltip' do
= sprite_icon("status_#{status}", size: 16, css_class: "has-tooltip ci-status-icon ci-status-icon-#{status}")
= domain.domain
- else
= link_to domain.domain, domain.url
%p
- if domain.subject
%span.label.label-gray Certificate: #{domain.subject}
- if domain.expired?
%span.label.label-danger Expired
- if verification_enabled && domain.unverified?
%li.warning-row
#{domain.domain} is not verified. To learn how to verify ownership, visit your
= link_to 'domain details', project_pages_domain_path(@project, domain)
- page_title "#{@domain.domain}", 'Pages Domains'
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if verification_enabled && @domain.unverified?
%p.alert.alert-warning
%strong
This domain is not verified. You will need to verify ownership before
access is enabled.
%h3.page-title
Pages Domain
......@@ -15,9 +21,26 @@
DNS
%td
%p
To access the domain create a new DNS record:
To access this domain create a new DNS record:
%pre
#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}.
- if verification_enabled
%tr
%td
Verification status
%td
%p
- help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
To #{link_to 'verify ownership', help_link} of your domain, create
this DNS record:
%pre
#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}
%p
- if @domain.verified?
#{@domain.domain} has been successfully verified.
- else
= button_to 'Verify ownership', verify_project_pages_domain_path(@project, @domain), class: 'btn btn-save btn-sm'
%tr
%td
Certificate
......
......@@ -80,6 +80,6 @@
- if show_new_ide?(@project)
= succeed " " do
= link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
= ide_edit_text
= _('Web IDE')
= render 'projects/buttons/download', project: @project, ref: @ref
......@@ -8,7 +8,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'boards'
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
......
......@@ -3,6 +3,7 @@
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
- cronjob:import_export_project_cleanup
- cronjob:pages_domain_verification_cron
- cronjob:pipeline_schedule
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
......@@ -82,6 +83,7 @@
- new_merge_request
- new_note
- pages
- pages_domain_verification
- post_receive
- process_commit
- project_cache
......
class AuthorizedProjectsWorker
include ApplicationWorker
prepend WaitableWorker
# Schedules multiple jobs and waits for them to be completed.
def self.bulk_perform_and_wait(args_list)
# Short-circuit: it's more efficient to do small numbers of jobs inline
return bulk_perform_inline(args_list) if args_list.size <= 3
waiter = Gitlab::JobWaiter.new(args_list.size)
# Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
# into [[1, "key"], [2, "key"], [3, "key"]]
waiting_args_list = args_list.map { |args| [*args, waiter.key] }
bulk_perform_async(waiting_args_list)
waiter.wait
end
# Performs multiple jobs directly. Failed jobs will be put into sidekiq so
# they can benefit from retries
def self.bulk_perform_inline(args_list)
failed = []
args_list.each do |args|
begin
new.perform(*args)
rescue
failed << args
end
end
bulk_perform_async(failed) if failed.present?
end
def perform(user_id, notify_key = nil)
def perform(user_id)
user = User.find_by(id: user_id)
user&.refresh_authorized_projects
ensure
Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
end
end
module WaitableWorker
extend ActiveSupport::Concern
module ClassMethods
# Schedules multiple jobs and waits for them to be completed.
def bulk_perform_and_wait(args_list, timeout: 10)
# Short-circuit: it's more efficient to do small numbers of jobs inline
return bulk_perform_inline(args_list) if args_list.size <= 3
waiter = Gitlab::JobWaiter.new(args_list.size)
# Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
# into [[1, "key"], [2, "key"], [3, "key"]]
waiting_args_list = args_list.map { |args| [*args, waiter.key] }
bulk_perform_async(waiting_args_list)
waiter.wait(timeout)
end
# Performs multiple jobs directly. Failed jobs will be put into sidekiq so
# they can benefit from retries
def bulk_perform_inline(args_list)
failed = []
args_list.each do |args|
begin
new.perform(*args)
rescue
failed << args
end
end
bulk_perform_async(failed) if failed.present?
end
end
def perform(*args)
notify_key = args.pop if Gitlab::JobWaiter.key?(args.last)
super(*args)
ensure
Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
end
end
class PagesDomainVerificationCronWorker
include ApplicationWorker
include CronjobQueue
def perform
PagesDomain.needs_verification.find_each do |domain|
PagesDomainVerificationWorker.perform_async(domain.id)
end
end
end
class PagesDomainVerificationWorker
include ApplicationWorker
def perform(domain_id)
domain = PagesDomain.find_by(id: domain_id)
return unless domain
VerifyPagesDomainService.new(domain).execute
end
end
......@@ -29,25 +29,23 @@ class StuckImportJobsWorker
end
def mark_projects_with_jid_as_failed!
completed_jids_count = 0
enqueued_projects_with_jid.find_in_batches(batch_size: 500) do |group|
jids = group.map(&:import_jid)
jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
return unless completed_jids.any?
if completed_jids.any?
completed_jids_count += completed_jids.count
group.each do |project|
project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid)
end
completed_project_ids = jids_and_ids.values_at(*completed_jids)
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}")
end
end
# We select the projects again, because they may have transitioned from
# scheduled/started to finished/failed while we were looking up their Sidekiq status.
completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids)
completed_jids_count
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}")
completed_projects.each do |project|
project.mark_import_as_failed(error_message)
end.count
end
def enqueued_projects
......
---
title: Use unique keys for token inputs while add same value twice to an epic
merge_request: 4618
author:
type: fixed
---
title: Add verification for GitLab Pages custom domains
merge_request:
author:
type: security
---
title: Improve query performance of MembersFinder.
merge_request: 17190
author:
type: performance
---
title: Enable Legacy Authorization by default on Cluster creations
merge_request: 17302
author:
type: fixed
---
title: Allows the usage of /milestone quick action for group milestones
merge_request: 17239
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Improve query performance for snippets dashboard.
merge_request: 17088
author:
type: performance
---
title: Fix issue with cache key being empty when variable used as the key
merge_request: 17260
author:
type: fixed
---
title: Prevent MR Widget error when no CI configured
title: Verify project import status again before marking as failed
merge_request:
author:
type: fixed
---
title: Fix 500 error when loading an invalid upload URL
merge_request:
author:
type: fixed
---
title: Increase feature flag cache TTL to one hour
merge_request:
author:
type: performance
---
title: Only check LFS integrity for first ref in a push to avoid timeout
merge_request: 17098
author:
type: performance
---
title: Fix single digit value clipping for stacked progress bar
merge_request: 17217
author:
type: fixed
---
title: Prevent trace artifact migration to incur data loss
merge_request: 17313
author:
type: fixed
---
title: Don't attempt to update user tracked fields if database is in read-only
merge_request:
author:
type: fixed
---
title: Improve performance of searching for and autocompleting of users
merge_request:
author:
type: performance
......@@ -249,6 +249,10 @@ production: &base
repository_archive_cache_worker:
cron: "0 * * * *"
# Verify custom GitLab Pages domains
pages_domain_verification_cron_worker:
cron: "*/15 * * * *"
##
# GitLab EE only jobs:
......
......@@ -517,6 +517,10 @@ Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *'
Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker'
Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
#
# GitLab Shell
#
......
......@@ -55,7 +55,11 @@ constraints(ProjectUrlConstrainer.new) do
end
resource :pages, only: [:show, :destroy] do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} }
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
member do
post :verify
end
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
......
......@@ -67,6 +67,7 @@
- [gcp_cluster, 1]
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
# EE-specific queues
- [ldap_group_sync, 2]
......
......@@ -54,32 +54,16 @@ var config = {
},
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
burndown_chart: './burndown_chart/index.js',
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
deploy_keys: './deploy_keys/index.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
geo_nodes: 'ee/geo_nodes',
help: './help/help.js',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
issue_show: './issue_show/index.js',
ldap_group_links: './groups/ldap_group_links.js',
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
mirrors: './mirrors',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
......@@ -89,27 +73,44 @@ var config = {
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches',
ee_protected_branches: 'ee/protected_branches',
protected_tags: './protected_tags',
ee_protected_tags: 'ee/protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: './service_desk_issues/index.js',
registry_list: './registry/index.js',
roadmap: 'ee/roadmap',
ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js',
ee_sidebar: 'ee/sidebar/sidebar_bundle.js',
snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
ui_development_kit: './ui_development_kit.js',
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
locale: './locale/index.js',
main: './main.js',
ide: './ide/index.js',
raven: './raven/index.js',
test: './test.js',
u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js',
// EE-only
add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
burndown_chart: './burndown_chart/index.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
geo_nodes: 'ee/geo_nodes',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
ldap_group_links: './groups/ldap_group_links.js',
mirrors: './mirrors',
ee_protected_branches: 'ee/protected_branches',
ee_protected_tags: 'ee/protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: './service_desk_issues/index.js',
roadmap: 'ee/roadmap',
ee_sidebar: 'ee/sidebar/sidebar_bundle.js',
},
output: {
......@@ -271,8 +272,6 @@ var config = {
'environments_folder',
'filtered_search',
'groups',
'issuable',
'issue_show',
'merge_conflicts',
'monitoring',
'notebook_viewer',
......@@ -283,7 +282,6 @@ var config = {
'ide',
'schedule_form',
'schedules_index',
'service_desk',
'sidebar',
'vue_merge_request_widget',
],
......@@ -332,6 +330,7 @@ var config = {
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js',
// EE-only
'ee': path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
'ee_empty_states': path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
'ee_icons': path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
......
class AddPagesDomainVerification < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :pages_domains, :verified_at, :datetime_with_timezone
add_column :pages_domains, :verification_code, :string
end
end
class AddPagesDomainVerifiedAtIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :pages_domains, :verified_at
end
def down
remove_concurrent_index :pages_domains, :verified_at
end
end
class AllowDomainVerificationToBeDisabled < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :application_settings, :pages_domain_verification_enabled, :boolean, default: true, null: false
end
end
class AddPagesDomainEnabledUntil < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :pages_domains, :enabled_until, :datetime_with_timezone
end
end
class AddPagesDomainEnabledUntilIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :pages_domains, [:project_id, :enabled_until]
add_concurrent_index :pages_domains, [:verified_at, :enabled_until]
end
def down
remove_concurrent_index :pages_domains, [:verified_at, :enabled_until]
remove_concurrent_index :pages_domains, [:project_id, :enabled_until]
end
end
class PagesDomainsVerificationGracePeriod < ActiveRecord::Migration
DOWNTIME = false
class PagesDomain < ActiveRecord::Base
include EachBatch
end
# Allow this migration to resume if it fails partway through
disable_ddl_transaction!
def up
now = Time.now
grace = now + 30.days
PagesDomain.each_batch do |relation|
relation.update_all(verified_at: now, enabled_until: grace)
# Sleep 2 minutes between batches to not overload the DB with dead tuples
sleep(2.minutes) unless relation.reorder(:id).last == PagesDomain.reorder(:id).last
end
end
def down
# no-op
end
end
class FillPagesDomainVerificationCode < ActiveRecord::Migration
DOWNTIME = false
class PagesDomain < ActiveRecord::Base
include EachBatch
end
# Allow this migration to resume if it fails partway through
disable_ddl_transaction!
def up
PagesDomain.where(verification_code: [nil, '']).each_batch do |relation|
connection.execute(set_codes_sql(relation))
# Sleep 2 minutes between batches to not overload the DB with dead tuples
sleep(2.minutes) unless relation.reorder(:id).last == PagesDomain.reorder(:id).last
end
change_column_null(:pages_domains, :verification_code, false)
end
def down
change_column_null(:pages_domains, :verification_code, true)
end
private
def set_codes_sql(relation)
ids = relation.pluck(:id)
whens = ids.map { |id| "WHEN #{id} THEN '#{SecureRandom.hex(16)}'" }
<<~SQL
UPDATE pages_domains
SET verification_code =
CASE id
#{whens.join("\n")}
END
WHERE id IN(#{ids.join(',')})
SQL
end
end
class EnqueueVerifyPagesDomainWorkers < ActiveRecord::Migration
class PagesDomain < ActiveRecord::Base
include EachBatch
end
def up
PagesDomain.each_batch do |relation|
ids = relation.pluck(:id).map { |id| [id] }
PagesDomainVerificationWorker.bulk_perform_async(ids)
end
end
def down
# no-op
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment