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