Commit 6a98da7c authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'origin/master' into 3776-ci-view-for-sast

parents 0fbd6a86 6e627c85
......@@ -19,3 +19,4 @@ app/models/project_services/kubernetes_service.rb
ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/spec/**/*
......@@ -36,6 +36,7 @@ variables:
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@docker.elastic.co-elasticsearch-elasticsearch:9200"
EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master-ee.json
before_script:
- bundle --version
......@@ -134,6 +135,30 @@ stages:
<<: *rspec-metadata
<<: *use-pg
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
.rspec-metadata-ee: &rspec-metadata-ee
<<: *rspec-metadata
stage: test
script:
- export KNAPSACK_TEST_FILE_PATTERN="ee/spec/**{,/*/**}/*_spec.rb" KNAPSACK_GENERATE_REPORT=true CACHE_CLASSES=true
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]} CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- cp ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- knapsack rspec "-Ispec --color --format documentation --tag ~geo"
.rspec-ee-pg: &rspec-ee-pg
<<: *rspec-metadata-ee
<<: *use-pg
.rspec-ee-mysql: &rspec-ee-mysql
<<: *rspec-metadata-ee
<<: *use-mysql
.rspec-geo-pg-9-6: &rspec-metadata-pg-geo
<<: *rspec-metadata
<<: *use-pg-9-6-no-elasticsearch
......@@ -143,11 +168,7 @@ stages:
- export CACHE_CLASSES=true
- source scripts/prepare_postgres_fdw.sh
- scripts/gitaly-test-spawn
- bundle exec rspec --color --format documentation --tag geo spec/
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
- bundle exec rspec --color --format documentation --tag geo ee/spec/
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
......@@ -265,6 +286,8 @@ retrieve-tests-metadata:
- mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
- wget -O $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
update-tests-metadata:
<<: *tests-metadata-state
......@@ -280,8 +303,10 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg-ee_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
......@@ -346,7 +371,13 @@ setup-test-env:
- tmp/tests
- config/secrets.yml
# EE jobs
rspec-pg-ee 0 2: *rspec-ee-pg
rspec-pg-ee 1 2: *rspec-ee-pg
rspec-mysql-ee 0 2: *rspec-ee-mysql
rspec-mysql-ee 1 2: *rspec-ee-mysql
rspec-pg geo: *rspec-metadata-pg-geo
## EE jobs
rspec-pg 0 27: *rspec-metadata-pg
rspec-pg 1 27: *rspec-metadata-pg
......
......@@ -10,10 +10,9 @@ AllCops:
Exclude:
- 'vendor/**/*'
- 'node_modules/**/*'
- 'db/*'
- 'db/**/*'
- 'db/fixtures/**/*'
- 'db/geo/*'
- 'ee/db/geo/*'
- 'ee/db/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
......@@ -27,7 +26,6 @@ Style/MutableConstant:
Exclude:
- 'db/migrate/**/*'
- 'db/post_migrate/**/*'
- 'db/geo/migrate/**/*'
- 'ee/db/migrate/**/*'
- 'ee/db/post_migrate/**/*'
- 'ee/db/geo/migrate/**/*'
......@@ -46,3 +44,16 @@ Gitlab/ModuleWithInstanceVariables:
# We ignore spec helpers because it usually doesn't matter
- spec/support/**/*.rb
- features/steps/**/*.rb
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
- 'config/**/*'
- 'db/**/*'
- 'features/**/*'
- 'lib/**/*.rake'
- 'qa/**/*'
- 'spec/**/*'
- 'ee/db/**/*'
- 'ee/lib/**/*.rake'
- 'ee/spec/**/*'
......@@ -176,6 +176,7 @@ export default {
<loading-icon />
</div>
<board-new-issue
:group-id="groupId"
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
......@@ -191,6 +192,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
......
......@@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown {
valueAttribute: 'data-text',
},
],
hideOnClick: false,
};
}
......
......@@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
......@@ -14,5 +13,4 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
IGNORE_HIDING_CLASS,
};
import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
class DropDown {
constructor(list, config = {}) {
constructor(list, config = { }) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
this.eventWrapper = {};
this.hideOnClick = config.hideOnClick !== false;
if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
......@@ -37,15 +38,17 @@ class DropDown {
clickEvent(e) {
if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
if (e.target.closest(`.${IGNORE_CLASS}`)) return;
const selected = utils.closest(e.target, 'LI');
const selected = e.target.closest('li');
if (!selected) return;
this.addSelectedClass(selected);
e.preventDefault();
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
if (this.hideOnClick) {
this.hide();
}
const listEvent = new CustomEvent('click.dl', {
detail: {
......
......@@ -5,13 +5,10 @@ import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
import MilestoneSelect from './milestone_select';
import WeightSelect from 'ee/weight_select'; // eslint-disable-line import/first
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
subscriptionSelect();
new WeightSelect();
};
......@@ -24,6 +24,51 @@ export default class Issue {
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
}
// Listen to state changes in the Vue app
document.addEventListener('issuable_vue_app:change', (event) => {
this.updateTopState(event.detail.isClosed, event.detail.data);
});
}
/**
* This method updates the top area of the issue.
*
* Once the issue state changes, either through a click on the top area (jquery)
* or a click on the bottom area (Vue) we need to update the top area.
*
* @param {Boolean} isClosed
* @param {Array} data
* @param {String} issueFailMessage
*/
updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
}
} else {
flash(issueFailMessage);
}
}
initIssueBtnEventListeners() {
......@@ -44,34 +89,8 @@ export default class Issue {
url = $button.attr('href');
return axios.put(url)
.then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
const isClosed = $button.hasClass('btn-close');
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
}
} else {
flash(issueFailMessage);
}
const isClosed = $button.hasClass('btn-close');
this.updateTopState(isClosed, data);
})
.catch(() => flash(issueFailMessage))
.then(() => {
......
import _ from 'underscore';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import Flash from '../flash';
import AUTH_METHOD from './constants';
import { backOff } from '../lib/utils/common_utils';
......@@ -145,22 +147,24 @@ export default class MirrorPull {
if (selectedAuthType === AUTH_METHOD.SSH &&
!$sshPublicKey.text().trim()) {
this.$dropdownAuthType.disable();
$.ajax({
type: 'PUT',
url: projectMirrorAuthTypeEndpoint,
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(authTypeData),
axios.put(projectMirrorAuthTypeEndpoint, JSON.stringify(authTypeData), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
.done((res) => {
.then(({ data }) => {
// Show SSH public key container and fill in public key
this.toggleAuthWell(selectedAuthType);
this.toggleSSHAuthWellMessage(true);
this.setSSHPublicKey(res.import_data_attributes.ssh_public_key);
})
.fail(() => {
Flash('Something went wrong on our end.');
this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
this.$wellAuthTypeChanging.addClass('hidden');
this.$dropdownAuthType.enable();
})
.always(() => {
.catch(() => {
Flash(__('Something went wrong on our end.'));
this.$wellAuthTypeChanging.addClass('hidden');
this.$dropdownAuthType.enable();
});
......
......@@ -2,16 +2,18 @@
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __ } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
......@@ -22,6 +24,7 @@
discussionLockedWidget,
markdownField,
userAvatarLink,
loadingButton,
},
mixins: [
issuableStateMixin,
......@@ -30,9 +33,6 @@
return {
note: '',
noteType: constants.COMMENT,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
......@@ -43,6 +43,7 @@
'getUserData',
'getNoteableData',
'getNotesData',
'issueState',
]),
isLoggedIn() {
return this.getUserData.id;
......@@ -105,7 +106,7 @@
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
});
this.initAutoSave();
......@@ -117,6 +118,9 @@
'stopPolling',
'restartPolling',
'removePlaceholderNotes',
'closeIssue',
'reopenIssue',
'toggleIssueLocalState',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!_.isEmpty(note) && !isSubmitting) {
......@@ -126,6 +130,8 @@
}
},
handleSave(withIssueAction) {
this.isSubmitting = true;
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
......@@ -142,7 +148,6 @@
if (this.noteType === constants.DISCUSSION) {
noteData.data.note.type = constants.DISCUSSION_NOTE;
}
this.isSubmitting = true;
this.note = ''; // Empty textarea while being requested. Repopulate in catch
this.resizeTextarea();
this.stopPolling();
......@@ -184,13 +189,25 @@ Please check your network connection and try again.`;
this.toggleIssueState();
}
},
enableButton() {
this.isSubmitting = false;
},
toggleIssueState() {
this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
// This is out of scope for the Notes Vue component.
// It was the shortest path to update the issue state and relevant places.
const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
$(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
if (this.isIssueOpen) {
this.closeIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
Flash(__('Something went wrong while closing the issue. Please try again later'));
});
} else {
this.reopenIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
Flash(__('Something went wrong while reopening the issue. Please try again later'));
});
}
},
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
......@@ -368,15 +385,19 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</li>
</ul>
</div>
<button
type="button"
@click="handleSave(true)"
<loading-button
v-if="canUpdateIssue"
:class="actionButtonClassNames"
:loading="isSubmitting"
@click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button">
{{ issueActionButtonTitle }}
</button>
:label="issueActionButtonTitle"
/>
<button
type="button"
v-if="note.length"
......
......@@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
notesPath: notesDataset.notesPath,
markdownDocsPath: notesDataset.markdownDocsPath,
quickActionsDocsPath: notesDataset.quickActionsDocsPath,
closeIssuePath: notesDataset.closeIssuePath,
reopenIssuePath: notesDataset.reopenIssuePath,
},
};
},
......
......@@ -32,4 +32,7 @@ export default {
toggleAward(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true });
},
toggleIssueState(endpoint, data) {
return Vue.http.put(endpoint, data);
},
};
......@@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const closeIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.closeIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const reopenIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.reopenIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
data,
isClosed: getters.issueState === constants.CLOSED,
} });
document.dispatchEvent(event);
};
export const toggleIssueLocalState = ({ commit }, newState) => {
if (newState === constants.CLOSED) {
commit(types.CLOSE_ISSUE);
} else if (newState === constants.REOPENED) {
commit(types.REOPEN_ISSUE);
}
};
export const saveNote = ({ commit, dispatch }, noteData) => {
const { note } = noteData.data.note;
let placeholderText = note;
......
......@@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const issueState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
......@@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
......@@ -152,4 +152,12 @@ export default {
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
},
[types.CLOSE_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.CLOSED });
},
[types.REOPEN_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.REOPENED });
},
};
......@@ -33,9 +33,6 @@ import flash from '../flash';
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm);
}
......@@ -48,21 +45,6 @@ import flash from '../flash';
return this.saveForm();
}
beforeUpdateUsername() {
$('.loading-username', this).removeClass('hidden');
}
afterUpdateUsername() {
$('.loading-username', this).addClass('hidden');
$('button[type=submit]', this).enable();
}
onUpdateNotifs(e, data) {
return data.saved ?
flash(__('Notification settings saved'), 'notice') :
flash(__('Failed to save new settings'));
}
saveForm() {
const self = this;
const formData = new FormData(this.form[0]);
......
......@@ -42,7 +42,7 @@ export default function initSettingsPanels() {
if (location.hash) {
const $target = $(location.hash);
if ($target.length && $target.hasClass('.settings')) {
if ($target.length && $target.hasClass('settings')) {
expandSection($target);
}
}
......
......@@ -72,7 +72,7 @@
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
class="pull-right lock-edit"
type="button"
@click.prevent="toggleForm"
>
......
......@@ -39,7 +39,7 @@
required: false,
},
containerClass: {
type: String,
type: [String, Array, Object],
required: false,
default: 'btn btn-align-content',
},
......
......@@ -457,9 +457,11 @@ img.emoji {
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-16 { margin-top: 16px; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-8 { margin-left: 8px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-15 { margin-left: 15px; }
.prepend-left-default { margin-left: $gl-padding; }
......
......@@ -736,10 +736,6 @@
}
}
.droplab-item-ignore {
pointer-events: none;
}
.pika-single.animate-picker.is-bound,
.pika-single.animate-picker.is-bound.is-hidden {
/*
......
......@@ -182,6 +182,7 @@ label {
.help-block {
margin-bottom: 0;
margin-top: #{$grid-size / 2};
}
.gl-field-error {
......
......@@ -197,11 +197,18 @@
margin-left: 0;
}
a.edit-link:not([href]):hover {
color: rgba($avatar-border, .2);
}
.lock-edit, // uses same style, different js behaviour
.edit-link {
@extend .btn-blank;
color: $gl-text-color;
&:not([href]):hover {
color: rgba($avatar-border, .2);
&:hover {
text-decoration: underline;
color: $md-link-color;
}
}
}
......
......@@ -201,11 +201,6 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
.branch-message,
.ref-message {
display: none;
}
.ref::selection {
color: $placeholder-text-color;
}
......@@ -236,6 +231,17 @@ ul.related-merge-requests > li {
transform: translateY(0);
display: none;
margin-top: 4px;
// override dropdown item styles
.btn.btn-success {
@include btn-default;
@include btn-green;
border-style: solid;
border-width: 1px;
line-height: $line-height-base;
width: auto;
}
}
.create-merge-request-dropdown-toggle {
......@@ -245,66 +251,6 @@ ul.related-merge-requests > li {
margin-left: 0;
}
}
.droplab-item-ignore {
pointer-events: auto;
}
.create-item {
cursor: pointer;
margin: 0 1px;
&:hover,
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
}
li.divider {
margin: 8px 10px;
}
li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
}
.icon-container {
float: left;
i {
visibility: hidden;
}
}
.description {
padding-left: 22px;
}
input,
span {
margin: 4px 0 0;
}
}
}
.discussion-reply-holder .note-edit-form {
......
......@@ -126,8 +126,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def referenced_merge_requests
@merge_requests = @issue.referenced_merge_requests(current_user)
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
@merge_requests, @closed_by_merge_requests = ::Issues::FetchReferencedMergeRequestsService.new(project, current_user).execute(issue)
respond_to do |format|
format.json do
......
......@@ -160,7 +160,7 @@ module DiffHelper
end
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
if diff_file.deleted_file?
"file-deletion"
elsif diff_file.new_file?
"file-addition"
......
......@@ -116,6 +116,10 @@ class Commit
raw.id
end
def project_id
project.id
end
def ==(other)
other.is_a?(self.class) && raw == other.raw
end
......
......@@ -61,11 +61,8 @@ module Network
@reserved[i] = []
end
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
Gitlab::GitalyClient.allow_n_plus_1_calls do
commits_sort_by_ref.each do |commit|
place_chain(commit)
end
commits_sort_by_ref.each do |commit|
place_chain(commit)
end
# find parent spaces for not overlap lines
......
......@@ -6,18 +6,14 @@ class DeleteMergedBranchesService < BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438
Gitlab::GitalyClient.allow_n_plus_1_calls do
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
branches = project.repository.merged_branch_names
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
end
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
end
end
......
module Issues
class FetchReferencedMergeRequestsService < Issues::BaseService
def execute(issue)
referenced_merge_requests = issue.referenced_merge_requests(current_user)
referenced_merge_requests = Gitlab::IssuableSorter.sort(project, referenced_merge_requests) { |i| i.iid.to_s }
closed_by_merge_requests = issue.closed_by_merge_requests(current_user)
closed_by_merge_requests = Gitlab::IssuableSorter.sort(project, closed_by_merge_requests) { |i| i.iid.to_s }
[referenced_merge_requests, closed_by_merge_requests]
end
end
end
......@@ -161,10 +161,12 @@ module MergeRequests
merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
unless merge_request.title
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
return if merge_request.title.present?
if issue_iid.present?
merge_request.title = "Resolve #{issue_iid}"
merge_request.title += " \"#{branch_title}\"" unless branch_title.empty?
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title += " \"#{branch_title}\"" if branch_title.present?
end
end
......
......@@ -12,6 +12,8 @@
markdown_docs_path: help_page_path('user/markdown'),
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url,
close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
......@@ -21,30 +21,33 @@
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create merge request and branch
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create branch
%li.divider
%li.droplab-item-ignore
Branch name
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%span.js-branch-message.branch-message.droplab-item-ignore
%li.droplab-item-ignore
Source (branch or tag)
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%span.js-ref-message.ref-message.droplab-item-ignore
%li.droplab-item-ignore
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
Create merge request
.droplab-dropdown
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.menu-item
= icon('check', class: 'icon')
= _('Create merge request and branch')
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
.menu-item
= icon('check', class: 'icon')
= _('Create branch')
%li.divider.droplab-item-ignore
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%span.js-branch-message.help-block
.form-group
%label{ for: 'source-name' }
= _('Source (branch or tag)')
%input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%span.js-ref-message.help-block
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
= _('Create merge request')
---
title: 'Geo: Reset force_redownload flag after successful sync'
merge_request:
author:
type: fixed
---
title: Fix the background_upload configuration being ignored.
merge_request: 4507
author:
type: fixed
---
title: Bump Geo JWT timeout from 1 minute to 10 minutes
merge_request:
author:
type: performance
---
title: Group MRs on issue page by project and namespace.
merge_request: 8494
author: Jeff Stubler
---
title: Render modified icon for moved file in changes dropdown
merge_request:
author:
type: fixed
---
title: Fix close button on issues not working on mobile
merge_request:
author:
type: fixed
---
title: Fix settings panels not expanding when fragment hash linked
merge_request: 17074
author:
type: fixed
---
title: Allow including custom attributes in API responses
merge_request: 16526
author: Markus Koller
type: changed
---
title: Resolve PrepareUntrackedUploads PostgreSQL syntax error
merge_request: 17019
author:
type: fixed
---
title: LDAP Person no longer throws exception on invalid entry
merge_request:
author:
type: fixed
---
title: Cleanup new branch/merge request form in issues
merge_request: 16854
author:
type: fixed
......@@ -348,9 +348,9 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['remote_directory'] ||= nil
Settings.artifacts['object_store']['background_upload'] ||= true
Settings.artifacts['object_store']['enabled'] = false if Settings.artifacts['object_store']['enabled'].nil?
Settings.artifacts['object_store']['remote_directory'] ||= nil
Settings.artifacts['object_store']['background_upload'] = true if Settings.artifacts['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.artifacts['object_store']['connection']&.deep_stringify_keys!
......@@ -394,9 +394,9 @@ Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"))
Settings.lfs['object_store'] ||= Settingslogic.new({})
Settings.lfs['object_store']['enabled'] ||= false
Settings.lfs['object_store']['remote_directory'] ||= nil
Settings.lfs['object_store']['background_upload'] ||= true
Settings.lfs['object_store']['enabled'] = false if Settings.lfs['object_store']['enabled'].nil?
Settings.lfs['object_store']['remote_directory'] ||= nil
Settings.lfs['object_store']['background_upload'] = true if Settings.lfs['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.lfs['object_store']['connection']&.deep_stringify_keys!
......@@ -407,19 +407,12 @@ Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
Settings.uploads['object_store'] ||= Settingslogic.new({})
Settings.uploads['object_store']['enabled'] ||= false
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
Settings.uploads['object_store']['background_upload'] ||= true
Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.uploads['object_store']['connection']&.deep_stringify_keys!
#
# Uploads
#
Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
#
# Mattermost
#
......
#
# Monkey patching the https support for private urls
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/4879
#
module Fog
module Storage
class GoogleXML
class File < Fog::Model
module MonkeyPatch
def url(expires)
requires :key
collection.get_https_url(key, expires)
end
end
prepend MonkeyPatch
end
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SchedulePopulateUntrackedUploadsIfNeeded < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
class UntrackedFile < ActiveRecord::Base
include EachBatch
self.table_name = 'untracked_files_for_uploads'
end
def up
if table_exists?(:untracked_files_for_uploads)
process_or_remove_table
end
end
def down
# nothing
end
private
def process_or_remove_table
if UntrackedFile.all.empty?
drop_temp_table
else
schedule_populate_untracked_uploads_jobs
end
end
def drop_temp_table
drop_table(:untracked_files_for_uploads, if_exists: true)
end
def schedule_populate_untracked_uploads_jobs
say "Scheduling #{FOLLOW_UP_MIGRATION} background migration jobs since there are rows in untracked_files_for_uploads."
bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION)
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180206200543) do
ActiveRecord::Schema.define(version: 20180208183958) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -54,6 +54,7 @@ following locations:
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
......
......@@ -15,6 +15,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
......@@ -98,6 +99,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
......@@ -145,6 +147,7 @@ Parameters:
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
Example response:
......@@ -204,6 +207,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
......
......@@ -37,6 +37,7 @@ GET /projects
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......@@ -222,6 +223,7 @@ GET /users/:user_id/projects
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......@@ -390,6 +392,7 @@ GET /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```json
{
......@@ -674,6 +677,7 @@ GET /projects/:id/forks
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......
......@@ -302,7 +302,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -334,7 +335,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -358,7 +360,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......@@ -603,7 +606,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -635,7 +639,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -659,7 +664,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......@@ -901,7 +907,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -931,7 +938,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -953,7 +961,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......
......@@ -167,6 +167,12 @@ You can filter by [custom attributes](custom_attributes.md) with:
GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
You can include the users' [custom attributes](custom_attributes.md) in the response with:
```
GET /users?with_custom_attributes=true
```
## Single user
Get a single user.
......@@ -248,6 +254,12 @@ Parameters:
}
```
You can include the user's [custom attributes](custom_attributes.md) in the response with:
```
GET /users/:id?with_custom_attributes=true
```
## User creation
Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority).
......
......@@ -84,6 +84,7 @@ future GitLab releases.**
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
| **GITLAB_FEATURES** | 10.6 | all | The comma separated list of licensed features available for your instance and plan |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
## 9.0 Renaming
......
......@@ -28,9 +28,8 @@ we still need to merge changes from GitLab CE to EE. To help us get there,
we should make sure that we no longer edit CE files in place in order to
implement EE features.
Instead, all EE codes should be put inside the `ee/` top-level directory, and
tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to
technical limitation. The rest of codes should be as close as to the CE files.
Instead, all EE code should be put inside the `ee/` top-level directory. The
rest of the code should be as close to the CE files as possible.
[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
......@@ -318,7 +317,7 @@ When you're testing EE-only features, avoid adding examples to the
existing CE specs. Also do no change existing CE examples, since they
should remain working as-is when EE is running without a license.
Instead place EE specs in the `spec/ee/spec` folder.
Instead place EE specs in the `ee/spec` folder.
## JavaScript code in `assets/javascripts/`
......
# Query Count Limits
Each controller or API endpoint is allowed to execute up to 100 SQL queries. In
a production environment we'll only log an error in case this threshold is
exceeded, but in a test environment we'll raise an error instead.
Each controller or API endpoint is allowed to execute up to 100 SQL queries and
in test environments we'll raise an error when this threshold is exceeded.
## Solving Failing Tests
......
......@@ -17,6 +17,9 @@ would be `process_something`. If you're not sure what queue a worker uses,
you can find it using `SomeWorker.queue`. There is almost never a reason to
manually override the queue name using `sidekiq_options queue: :some_queue`.
You must always add any new queues to `app/workers/all_queues.yml` otherwise
your worker will not run.
## Queue Namespaces
While different workers cannot share a queue, they can share a queue namespace.
......
......@@ -136,7 +136,7 @@ learn more.
## EE-specific tests
EE-specific tests follows the same organization, but under the `spec/ee` folder.
EE-specific tests follows the same organization, but under the `ee/spec` folder.
## How to test at the correct level?
......
......@@ -395,27 +395,29 @@ data before running `pg_basebackup`.
gitlab-ctl replicate-geo-database --slot-name=secondary_example --host=1.2.3.4
```
When prompted, enter the password you set up for the `gitlab_replicator`
When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
user in the first step.
This command also takes a number of additional options. You can use `--help`
to list them all, but here are a couple of tips:
- If PostgreSQL is listening on a non-standard port, add `--port=` as well.
- If your database is too large to be transferred in 30 minutes, you will need
to increase the timeout, e.g., `--backup-timeout=3600` if you expect the
initial replication to take under an hour.
to increase the timeout, e.g., `--backup-timeout=3600` if you expect the
initial replication to take under an hour.
- Pass `--sslmode=disable` to skip PostgreSQL TLS authentication altogether
(e.g., you know the network path is secure, or you are using a site-to-site
VPN). This is **not** safe over the public Internet!
(e.g., you know the network path is secure, or you are using a site-to-site
VPN). This is **not** safe over the public Internet!
- You can read more details about each `sslmode` in the
[PostgreSQL documentation](https://www.postgresql.org/docs/9.6/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION);
the instructions above are carefully written to ensure protection against
both passive eavesdroppers and active "man-in-the-middle" attackers.
[PostgreSQL documentation](https://www.postgresql.org/docs/9.6/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION);
the instructions above are carefully written to ensure protection against
both passive eavesdroppers and active "man-in-the-middle" attackers.
- Change the `--slot-name` to the name of the replication slot
to be used on the primary database. The script will attempt to create the
replication slot automatically if it does not exist.
to be used on the primary database. The script will attempt to create the
replication slot automatically if it does not exist.
- If you're repurposing an old server into a Geo secondary, you'll need to
add `--force` to the command line.
add `--force` to the command line.
- When not in a production machine you can disable backup step if you
really sure this is what you want by adding `--skip-backup`
1. Verify that the secondary is configured correctly and that the primary is
reachable:
......
......@@ -6,7 +6,6 @@
and [problems](https://bugs.mysql.com/bug.php?id=65830) that
[suggested](https://bugs.mysql.com/bug.php?id=50909)
[fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152].
## Initial database setup
......
......@@ -169,9 +169,6 @@ from the UI.
If you want approvals to persist, independent of changes to the merge request,
turn this setting to off by unchecking the box and saving the changes.
If one of the approvers pushes a commit to the branch that is tied to the merge
request, they automatically get excluded from the approvers list.
## Merge requests with different source branch and target branch projects
If the merge request source branch and target branch belong to different
......
import Api from '~/api';
import { __ } from '~/locale';
import Flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
export default class ApproversSelect {
constructor() {
......@@ -147,7 +150,7 @@ export default class ApproversSelect {
}
static saveApprovers(fieldName) {
const $input = window.$(`[name="${fieldName}"]`);
const $input = $(`[name="${fieldName}"]`);
const newValue = $input.val();
const $loadWrapper = $('.load-wrapper');
const $approverSelect = $('.js-select-user-and-group');
......@@ -158,41 +161,42 @@ export default class ApproversSelect {
const $form = $('.js-add-approvers').closest('form');
$loadWrapper.removeClass('hidden');
window.$.ajax({
url: $form.attr('action'),
type: 'POST',
data: {
_method: 'PATCH',
[fieldName]: newValue,
},
success: ApproversSelect.updateApproverList,
complete() {
$input.val('');
$approverSelect.select2('val', '');
$loadWrapper.addClass('hidden');
},
error() {
window.Flash('Failed to add Approver', 'alert');
axios.post($form.attr('action'), `_method=PATCH&${[encodeURIComponent(fieldName)]}=${newValue}`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
}).then(({ data }) => {
ApproversSelect.updateApproverList(data);
ApproversSelect.saveApproversComplete($input, $approverSelect, $loadWrapper);
}).catch(() => {
Flash(__('An error occurred while adding approver'));
ApproversSelect.saveApproversComplete($input, $approverSelect, $loadWrapper);
});
}
static saveApproversComplete($input, $approverSelect, $loadWrapper) {
$input.val('');
$approverSelect.select2('val', '');
$loadWrapper.addClass('hidden');
}
static removeApprover(e) {
e.preventDefault();
const target = e.currentTarget;
const $loadWrapper = $('.load-wrapper');
$loadWrapper.removeClass('hidden');
$.ajax({
url: target.getAttribute('href'),
type: 'POST',
data: {
_method: 'DELETE',
},
success: ApproversSelect.updateApproverList,
complete: () => $loadWrapper.addClass('hidden'),
error() {
window.Flash('Failed to remove Approver', 'alert');
axios.post(target.getAttribute('href'), '_method=DELETE', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
}).then(({ data }) => {
ApproversSelect.updateApproverList(data);
$loadWrapper.addClass('hidden');
}).catch(() => {
Flash(__('An error occurred while removing approver'));
$loadWrapper.addClass('hidden');
});
}
......
import flash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default function initPathLocks(url, path) {
$('a.path-lock').on('click', (e) => {
e.preventDefault();
$.post(url, {
axios.post(url, {
path,
}, () => {
}).then(() => {
location.reload();
});
}).catch(() => flash(__('An error occurred while initializing path locks')));
});
}
......@@ -64,6 +64,15 @@ module EE
succeeded
end
override :features
def features
return super unless License.current
License.current.features.select do |feature|
License.global_feature?(feature) || feature_available?(feature)
end
end
# Checks features (i.e. https://about.gitlab.com/products/) availabily
# for a given Namespace plan. This method should consider ancestor groups
# being licensed.
......
......@@ -153,7 +153,7 @@ class License < ActiveRecord::Base
end
def plans_with_feature(feature)
if GLOBAL_FEATURES.include?(feature)
if global_feature?(feature)
raise ArgumentError, "Use `License.feature_available?` for features that cannot be restricted to only a subset of projects or namespaces"
end
......@@ -187,6 +187,10 @@ class License < ActiveRecord::Base
license
end
def global_feature?(feature)
GLOBAL_FEATURES.include?(feature)
end
end
def data_filename
......
......@@ -80,7 +80,7 @@ module Geo
url = Gitlab::Geo.primary_node.url + repository.full_path + '.git'
# Fetch the repository, using a JWT header for authentication
authorization = ::Gitlab::Geo::BaseRequest.new.authorization
authorization = ::Gitlab::Geo::RepoSyncRequest.new.authorization
header = { "http.#{url}.extraHeader" => "Authorization: #{authorization}" }
repository.with_config(header) do
......@@ -108,6 +108,7 @@ module Geo
attrs["resync_#{type}"] = false
attrs["#{type}_retry_count"] = nil
attrs["#{type}_retry_at"] = nil
attrs["force_to_redownload_#{type}"] = false
end
registry.update!(attrs)
......
module Geo
class FileUploadService < FileService
IAT_LEEWAY = 60.seconds.to_i
attr_reader :auth_header
def initialize(params, auth_header)
......
module Gitlab
module Ci
module External
module File
class Base
YAML_WHITELIST_EXTENSION = /(yml|yaml)$/i.freeze
def initialize(location, opts = {})
@location = location
end
def valid?
location.match(YAML_WHITELIST_EXTENSION) && content
end
def content
raise NotImplementedError, 'content must be implemented and return a string or nil'
end
def error_message
raise NotImplementedError, 'error_message must be implemented and return a string'
end
end
end
end
end
end
......@@ -2,27 +2,28 @@ module Gitlab
module Ci
module External
module File
class Local
class Local < Base
attr_reader :location, :project, :sha
def initialize(location, opts = {})
@location = location
super
@project = opts.fetch(:project)
@sha = opts.fetch(:sha)
end
def valid?
local_file_content
def content
@content ||= fetch_local_content
end
def content
local_file_content
def error_message
"Local file '#{location}' is not valid."
end
private
def local_file_content
@local_file_content ||= project.repository.blob_data_at(sha, location)
def fetch_local_content
project.repository.blob_data_at(sha, location)
end
end
end
......
......@@ -2,29 +2,25 @@ module Gitlab
module Ci
module External
module File
class Remote
class Remote < Base
include Gitlab::Utils::StrongMemoize
attr_reader :location
def initialize(location, opts = {})
@location = location
end
def valid?
::Gitlab::UrlSanitizer.valid?(location) && content
end
def content
return @content if defined?(@content)
@content = strong_memoize(:content) do
begin
HTTParty.get(location)
rescue HTTParty::Error, Timeout::Error
false
rescue HTTParty::Error, Timeout::Error, SocketError
nil
end
end
end
def error_message
"Remote file '#{location}' is not valid."
end
end
end
end
......
......@@ -17,10 +17,8 @@ module Gitlab
attr_reader :locations, :project, :sha
def build_external_file(location)
remote_file = Gitlab::Ci::External::File::Remote.new(location)
if remote_file.valid?
remote_file
if ::Gitlab::UrlSanitizer.valid?(location)
Gitlab::Ci::External::File::Remote.new(location)
else
options = { project: project, sha: sha }
Gitlab::Ci::External::File::Local.new(location, options)
......
......@@ -28,7 +28,7 @@ module Gitlab
def validate_external_file(external_file)
unless external_file.valid?
raise FileError, "External file: '#{external_file.location}' should be a valid local or remote file"
raise FileError, external_file.error_message
end
end
......
......@@ -77,6 +77,7 @@ module Gitlab
extname = File.extname(filename)
basename = filename.sub(/#{extname}$/, '')
content = result["_source"]["blob"]["content"]
project_id = result["_parent"].to_i
total_lines = content.lines.size
term =
......@@ -113,7 +114,8 @@ module Gitlab
basename: basename,
ref: ref,
startline: from + 1,
data: data.join
data: data.join,
project_id: project_id
)
end
......
......@@ -20,6 +20,10 @@ module Gitlab
geo_auth_token(request_data)
end
def expiration_time
1.minute
end
private
def geo_auth_token(message)
......@@ -27,6 +31,7 @@ module Gitlab
raise GeoNodeNotFoundError unless geo_node
token = JSONWebToken::HMACToken.new(geo_node.secret_access_key)
token.expire_time = Time.now + expiration_time
token[:data] = message.to_json
"#{GITLAB_GEO_AUTH_TOKEN_TYPE} #{geo_node.access_key}:#{token.encoded}"
......
module Gitlab
module Geo
class RepoSyncRequest < BaseRequest
def expiration_time
10.minutes
end
end
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.
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