summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Bennett <lbennett@gitlab.com>2018-01-24 22:10:31 +0000
committerLuke Bennett <lbennett@gitlab.com>2018-01-24 22:10:31 +0000
commitece43ee69ae95be2f74ee50302fa4aebbf623ff6 (patch)
tree166913f319cc3d2ba9d63bfd045c3eded9b6b5b1
parent128ef10b377a4faaa4d768c0b219d879f6a86a36 (diff)
parent668d81a07231be584533ea043b76c675b113a1bb (diff)
downloadgitlab-ce-ece43ee69ae95be2f74ee50302fa4aebbf623ff6.tar.gz
Merge branch '10-4-stable-patch-1' into '10-4-stable'
Prepare 10.4.1 release See merge request gitlab-org/gitlab-ce!16629
-rw-r--r--app/assets/javascripts/api.js1
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue9
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue11
-rw-r--r--app/assets/javascripts/ide/ide_router.js4
-rw-r--r--app/assets/javascripts/ide/lib/editor.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js35
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js18
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue18
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_icon.vue4
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/repo.scss74
-rw-r--r--app/controllers/concerns/group_tree.rb6
-rw-r--r--app/finders/group_descendants_finder.rb41
-rw-r--r--app/finders/labels_finder.rb2
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/webpack_helper.rb14
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/route.rb12
-rw-r--r--app/views/ide/index.html.haml5
-rw-r--r--app/views/layouts/nav_only.html.haml5
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml5
-rw-r--r--app/views/projects/jobs/show.html.haml10
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml6
-rw-r--r--changelogs/unreleased/42159-utf8-uploads.yml5
-rw-r--r--changelogs/unreleased/bvl-parent-preloading.yml5
-rw-r--r--changelogs/unreleased/fix-redirect-routes-schema.yml5
-rw-r--r--changelogs/unreleased/issue_37143_2.yml5
-rw-r--r--changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml6
-rw-r--r--config/webpack.config.js7
-rw-r--r--db/migrate/20180113220114_rework_redirect_routes_indexes.rb68
-rw-r--r--db/schema.rb4
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md6
-rw-r--r--doc/ci/examples/dast.md21
-rw-r--r--doc/ci/runners/README.md22
-rw-r--r--doc/user/project/clusters/index.md276
-rw-r--r--doc/user/project/integrations/kubernetes.md10
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedule_play.pngbin0 -> 39142 bytes
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedules_list.pngbin14665 -> 38034 bytes
-rw-r--r--doc/user/project/pipelines/schedules.md15
-rw-r--r--lib/banzai/filter/relative_link_filter.rb6
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--spec/controllers/dashboard/groups_controller_spec.rb20
-rw-r--r--spec/controllers/groups/children_controller_spec.rb24
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb8
-rw-r--r--spec/features/issues/spam_issues_spec.rb3
-rw-r--r--spec/features/projects/jobs_spec.rb20
-rw-r--r--spec/finders/group_descendants_finder_spec.rb20
-rw-r--r--spec/helpers/issuables_helper_spec.rb29
-rw-r--r--spec/helpers/projects_helper_spec.rb22
-rw-r--r--spec/javascripts/flash_spec.js12
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js33
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js6
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/loading_icon_spec.js3
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb17
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/route_spec.rb61
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml2
80 files changed, 932 insertions, 191 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 21d8c790e90..38c67b5f04e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -218,6 +218,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus;
+ if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error);
},
);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 44deab9288e..a0af2875ab5 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
+ if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, {
once: true,
passive: true,
@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document,
actionConfig = null,
fadeTransition = true,
+ addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block';
+ if (addBodyClass) document.body.classList.add('flash-shown');
+
return flashContainer;
};
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 979721dcb5a..932fa60e50d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -66,12 +66,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
- this.$store.dispatch('getTreeData', {
- projectId: this.currentProjectId,
- branch: this.currentBranchId,
- endpoint: `/tree/${this.currentBranchId}`,
- force: true,
- });
+ this.commitMessage = '';
+ this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
@@ -150,6 +146,7 @@ export default {
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
+ :class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 343fd0a5300..b377df30837 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -38,7 +38,10 @@ export default {
this.editor.createInstance(this.$refs.editor);
})
.then(() => this.setupEditor())
- .catch(() => flash('Error setting up monaco. Please try again.'));
+ .catch((err) => {
+ flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
+ throw err;
+ });
},
setupEditor() {
if (!this.activeFile) return;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index c8b0441d81c..f0323ff18ac 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -35,9 +35,12 @@
return this.file.type === 'tree';
},
levelIndentation() {
- return {
- marginLeft: `${this.file.level * 16}px`,
- };
+ if (this.file.level > 0) {
+ return {
+ marginLeft: `${this.file.level * 16}px`,
+ };
+ }
+ return {};
},
shortId() {
return this.file.id.substr(0, 8);
@@ -111,7 +114,7 @@
:parent="file"/>
<i
class="fa"
- v-if="changedClass"
+ v-if="file.changed || file.tempFile"
:class="changedClass"
aria-hidden="true"
>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a9cbf8e370f..a7fb9e0588a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
}
})
.catch((e) => {
- flash('Error while loading the branch files. Please try again.');
+ flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
})
.catch((e) => {
- flash('Error while loading the project data. Please try again.');
+ flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 51e202b9348..ce4e81ffe5e 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -54,7 +54,7 @@ export default class Editor {
attachModel(model) {
this.instance.setModel(model.getModel());
- this.dirtyDiffController.attachModel(model);
+ if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
@@ -67,7 +67,7 @@ export default class Editor {
return acc;
}, {}));
- this.dirtyDiffController.reDecorate(model);
+ if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 335882bb6d7..96a87744df5 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
+import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false;
})
- .catch(() => flash('Error checking branch data. Please try again.'));
+ .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = (
{ commit, state, dispatch, getters },
@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
- flash(data.message);
+ flash(data.message, 'alert', document, null, false, true);
return;
}
@@ -105,19 +106,25 @@ export const commitChanges = (
},
};
+ let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
+ if (data.stats) {
+ commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
+ }
+
flash(
- `Your changes have been committed. Commit ${data.short_id} with ${
- data.stats.additions
- } additions, ${data.stats.deletions} deletions.`,
+ commitMsg,
'notice',
- );
+ document,
+ null,
+ false,
+ true);
+ window.dispatchEvent(new Event('resize'));
if (newMr) {
+ dispatch('discardAllChanges');
dispatch(
'redirectToUrl',
- `${
- selectedProject.web_url
- }/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
+ `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
@@ -134,12 +141,18 @@ export const commitChanges = (
});
dispatch('discardAllChanges');
- dispatch('closeAllFiles');
window.scrollTo(0, 0);
}
})
- .catch(() => flash('Error committing changes. Please try again.'));
+ .catch((err) => {
+ let errMsg = 'Error committing changes. Please try again.';
+ if (err.responseJSON && err.responseJSON.message) {
+ errMsg += ` (${stripHtml(err.responseJSON.message)})`;
+ }
+ flash(errMsg, 'alert', document, null, false, true);
+ window.dispatchEvent(new Event('resize'));
+ });
export const createTempEntry = (
{ state, dispatch },
diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js
index 32bdf7fec22..589ec28c6a4 100644
--- a/app/assets/javascripts/ide/stores/actions/branch.js
+++ b/app/assets/javascripts/ide/stores/actions/branch.js
@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data);
})
.catch(() => {
- flash('Error loading branch data. Please try again.');
+ flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 0f27d5bf1c3..670af2fb89e 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
- flash('Error loading file data. Please try again.');
+ flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
- .catch(() => flash('Error loading file content. Please try again.'));
+ .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
- commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
+ if (state.selectedFile) {
+ commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
+ }
};
export const setFileEOL = ({ state, commit }, { eol }) => {
- commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
+ if (state.selectedFile) {
+ commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
+ }
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
- commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
+ if (state.selectedFile) {
+ commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
+ }
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl,
});
- if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+ if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 02d4bd87ab0..faeceb430a2 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data);
})
.catch(() => {
- flash('Error loading project data. Please try again.');
+ flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 25909400a75..302ba45edee 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data);
})
.catch((e) => {
- flash('Error loading tree data. Please try again.');
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree);
})
- .catch(() => flash('Error fetching log data.'));
+ .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 5f3655b0092..72db1c180c9 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -64,7 +64,7 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
- content: '',
+ content: file.raw,
changed: false,
});
},
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index fc10a43d1bf..93e6def172a 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -143,6 +143,14 @@ export default {
hasUpdated() {
return !!this.state.updatedAt;
},
+ issueChanged() {
+ const descriptionChanged =
+ this.initialDescriptionText !== this.store.formState.description;
+ const titleChanged =
+ this.initialTitleText !== this.store.formState.title;
+ return descriptionChanged || titleChanged;
+ },
+
},
components: {
descriptionComponent,
@@ -156,6 +164,14 @@ export default {
],
methods: {
+ handleBeforeUnloadEvent(e) {
+ const event = e;
+ if (this.showForm && this.issueChanged) {
+ event.returnValue = 'Are you sure you want to lose your issue information?';
+ }
+ return undefined;
+ },
+
openForm() {
if (!this.showForm) {
this.showForm = true;
@@ -243,12 +259,14 @@ export default {
}
});
+ window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
+ window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index cb6e06ea584..62d80c4a649 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
-export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
+export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
index 5648208f7b1..587de731f6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" />
+ <status-icon status="warning" />
<div class="media-body space-children">
<span class="bold">
<template v-if="mr.mergeError">{{mr.mergeError}}.</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index dd8b2665b1d..dc19b20aa11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -12,7 +12,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" />
+ <status-icon status="warning" />
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index 5d468a085cb..7a887bacfa7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -11,7 +11,7 @@ export default {
template: `
<div class="mr-widget-body media">
<status-icon
- status="failed"
+ status="warning"
:show-disabled-button="true" />
<div class="media-body space-children">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index c25d6c359bb..fc5f18695b7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 1bc0b7e0819..16ff1109e3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 1cedf86e811..2c84f423ee2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index 6853ba4b9f8..cbaa73deffa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index f16414ad5c0..e51eef07093 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -69,7 +69,7 @@ export default {
},
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
- return 'failed';
+ return 'warning';
}
return 'success';
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index af19cf6ab87..46687cc85e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index a119ecbbdfe..97b1940f4be 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="true" />
+ <status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index 13461440ef2..b4b0f00445c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -37,7 +37,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
+ <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue
index 15581d5c2a0..d7044486510 100644
--- a/app/assets/javascripts/vue_shared/components/loading_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue
@@ -32,8 +32,8 @@
</script>
<template>
<component
- :is="this.rootElementType"
- class="text-center">
+ :is="rootElementType"
+ class="loading-container text-center">
<i
class="fa fa-spin fa-spinner"
:class="cssClass"
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f7853909f56..bfb2d9c3ab7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -258,6 +258,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
+$flash-height: 52px;
+$context-header-height: 60px;
/*
* Common component specific colors
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6cb32408a48..84e3e422d92 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -113,6 +113,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle;
margin-right: 2px;
}
+
+ .loading-container {
+ margin-right: 4px;
+ display: inline-block;
+ }
}
.multi-file-table-col-commit-message {
@@ -253,7 +258,6 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
- height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
@@ -262,6 +266,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar {
display: flex;
flex-direction: column;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+ }
}
.multi-file-commit-panel-inner {
@@ -502,19 +511,70 @@ table.table tr td.multi-file-table-name {
}
}
-.ide-flash-container.flash-container {
- margin-top: $header-height;
- margin-bottom: 0;
+.ide.nav-only {
+ .flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+ }
+
+ .alert-wrapper .flash-container .flash-alert:last-child,
+ .alert-wrapper .flash-container .flash-notice:last-child {
+ margin-bottom: 0;
+ }
+
+ .content {
+ margin-top: $header-height;
+ }
+
+ .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
+ max-height: calc(100vh - #{$header-height + $context-header-height});
+ }
+
+ &.flash-shown {
+ .content {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $flash-height});
+ }
+
+ .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
+ max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
+ }
+ }
}
-.with-performance-bar {
- .ide-flash-container.flash-container {
- margin-top: $header-height + $performance-bar-height;
+.with-performance-bar .ide.nav-only {
+ .flash-container {
+ margin-top: #{$header-height + $performance-bar-height};
+ }
+
+ .content {
+ margin-top: #{$header-height + $performance-bar-height};
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
+
+ .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
+ max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
+ }
+
+ &.flash-shown {
+ .content {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
+ }
+
+ .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
+ max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
+ }
+ }
}
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index b10147835f3..6d564d4642c 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -2,7 +2,11 @@ module GroupTree
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
@groups = if params[:filter].present?
- Gitlab::GroupHierarchy.new(groups.search(params[:filter]))
+ # We find the ancestors by ID of the search results here.
+ # Otherwise the ancestors would also have filters applied,
+ # which would cause them not to be preloaded.
+ group_ids = groups.search(params[:filter]).select(:id)
+ Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
.base_and_ancestors
else
# Only show root groups if no parent-id is given
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 1a5f6063437..9e6cdd3c5a5 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -27,12 +27,16 @@ class GroupDescendantsFinder
end
def execute
- # The children array might be extended with the ancestors of projects when
- # filtering. In that case, take the maximum so the array does not get limited
- # Otherwise, allow paginating through all results
+ # The children array might be extended with the ancestors of projects and
+ # subgroups when filtering. In that case, take the maximum so the array does
+ # not get limited otherwise, allow paginating through all results.
#
all_required_elements = children
- all_required_elements |= ancestors_for_projects if params[:filter]
+ if params[:filter]
+ all_required_elements |= ancestors_of_filtered_subgroups
+ all_required_elements |= ancestors_of_filtered_projects
+ end
+
total_count = [all_required_elements.size, paginator.total_count].max
Kaminari.paginate_array(all_required_elements, total_count: total_count)
@@ -49,8 +53,11 @@ class GroupDescendantsFinder
end
def paginator
- @paginator ||= Gitlab::MultiCollectionPaginator.new(subgroups, projects,
- per_page: params[:per_page])
+ @paginator ||= Gitlab::MultiCollectionPaginator.new(
+ subgroups,
+ projects.with_route,
+ per_page: params[:per_page]
+ )
end
def direct_child_groups
@@ -93,15 +100,21 @@ class GroupDescendantsFinder
#
# So when searching 'project', on the 'subgroup' page we want to preload
# 'nested-group' but not 'subgroup' or 'root'
- def ancestors_for_groups(base_for_ancestors)
- Gitlab::GroupHierarchy.new(base_for_ancestors)
+ def ancestors_of_groups(base_for_ancestors)
+ group_ids = base_for_ancestors.except(:select, :sort).select(:id)
+ Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
.base_and_ancestors(upto: parent_group.id)
end
- def ancestors_for_projects
+ def ancestors_of_filtered_projects
projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
- ancestors_for_groups(groups_to_load_ancestors_of)
+ ancestors_of_groups(groups_to_load_ancestors_of)
+ .with_selects_for_list(archived: params[:archived])
+ end
+
+ def ancestors_of_filtered_subgroups
+ ancestors_of_groups(subgroups)
.with_selects_for_list(archived: params[:archived])
end
@@ -111,7 +124,7 @@ class GroupDescendantsFinder
# When filtering subgroups, we want to find all matches withing the tree of
# descendants to show to the user
groups = if params[:filter]
- ancestors_for_groups(subgroups_matching_filter)
+ subgroups_matching_filter
else
direct_child_groups
end
@@ -119,8 +132,10 @@ class GroupDescendantsFinder
end
def direct_child_projects
- GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
- .execute
+ GroupProjectsFinder.new(group: parent_group,
+ current_user: current_user,
+ options: { only_owned: true },
+ params: params).execute
end
# Finds all projects nested under `parent_group` or any of its descendant
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 6de9eb89468..1427cdaa382 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -71,7 +71,7 @@ class LabelsFinder < UnionFinder
end
def projects?
- params[:project_ids].present?
+ params[:project_ids]
end
def only_group_labels?
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 2668cf78afe..520a875238c 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -304,6 +304,12 @@ module IssuablesHelper
issuable.model_name.human.downcase
end
+ def selected_labels
+ Array(params[:label_name]).map do |label_name|
+ Label.new(title: label_name)
+ end
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 33453dd178f..94887c2cbd2 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,12 +1,12 @@
require 'webpack/rails/manifest'
module WebpackHelper
- def webpack_bundle_tag(bundle)
- javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
+ def webpack_bundle_tag(bundle, force_same_domain: false)
+ javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end
# override webpack-rails gem helper until changes can make it upstream
- def gitlab_webpack_asset_paths(source, extension: nil)
+ def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present?
paths = Webpack::Rails::Manifest.asset_paths(source)
@@ -14,9 +14,11 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" }
end
- force_host = webpack_public_host
- if force_host
- paths.map! { |p| "#{force_host}#{p}" }
+ unless force_same_domain
+ force_host = webpack_public_host
+ if force_host
+ paths.map! { |p| "#{force_host}#{p}" }
+ end
end
paths
diff --git a/app/models/project.rb b/app/models/project.rb
index fbe65e700a4..53a306b8258 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1023,6 +1023,8 @@ class Project < ActiveRecord::Base
end
def fork_source
+ return nil unless forked?
+
forked_from_project || fork_network&.root_project
end
diff --git a/app/models/route.rb b/app/models/route.rb
index 412f5fb45a5..3d4b5a8b5ee 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -1,4 +1,6 @@
class Route < ActiveRecord::Base
+ include CaseSensitivity
+
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true
@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base
validate :ensure_permanent_paths, if: :path_changed?
+ before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end
+
+ def delete_conflicting_orphaned_routes
+ conflicting = self.class.iwhere(path: path)
+ conflicting_orphaned_routes = conflicting.select do |route|
+ route.source.nil?
+ end
+
+ conflicting_orphaned_routes.each(&:destroy)
+ end
end
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index cb413f197de..3dbdfc97654 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,10 +1,9 @@
+- @body_class = 'ide'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'ide'
-
-.ide-flash-container.flash-container
+ = webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center
diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/nav_only.html.haml
index 6fa4b39dc10..0811211f7b2 100644
--- a/app/views/layouts/nav_only.html.haml
+++ b/app/views/layouts/nav_only.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
+ %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } }
= render 'peek/bar'
= render "layouts/header/default"
= render 'shared/outdated_browser'
@@ -10,4 +10,5 @@
= render "layouts/broadcast"
= yield :flash_message
= render "layouts/flash"
- = yield
+ .content{ id: "content-body" }
+ = yield
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
index 311934d9c33..c66313bdbf3 100644
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ b/app/views/projects/jobs/_empty_state.html.haml
@@ -1,7 +1,7 @@
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
-- content = local_assigns.fetch(:content, nil)
+- content = local_assigns.fetch(:content)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
@@ -11,8 +11,7 @@
.col-xs-12
.text-content
%h4.text-center= title
- - if content
- %p= content
+ %p= content
- if action
.text-center
= action
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 1e6d6f67e66..eb0773f2d4e 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -95,12 +95,18 @@
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments'),
action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), method: :post, class: 'btn btn-primary', title: _('Trigger this manual action') )
+ - elsif @build.created?
+ = render 'empty_state',
+ illustration: 'illustrations/job_not_triggered.svg',
+ illustration_size: 'svg-306',
+ title: _('This job has not been triggered yet'),
+ content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
- else
= render 'empty_state',
illustration: 'illustrations/job_not_triggered.svg',
illustration_size: 'svg-306',
- title: _('This job has not been triggered yet')
-
+ title: _('This job has not started yet'),
+ content: _('This job is in pending state and is waiting to be picked by a runner')
= render "sidebar"
.js-build-options{ data: javascript_build_options }
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 8442d7ff4a2..7704c88905b 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -22,7 +22,7 @@
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
+ = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- if issuable_filter_present?
.filter-item.inline.reset-filters
diff --git a/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml b/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml
new file mode 100644
index 00000000000..96bb59d303c
--- /dev/null
+++ b/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml
@@ -0,0 +1,6 @@
+---
+title: Fix bug in which projects with forks could not change visibility settings from
+ Private to Public
+merge_request: 16595
+author:
+type: fixed
diff --git a/changelogs/unreleased/42159-utf8-uploads.yml b/changelogs/unreleased/42159-utf8-uploads.yml
new file mode 100644
index 00000000000..f6eba8f28f5
--- /dev/null
+++ b/changelogs/unreleased/42159-utf8-uploads.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly escape UTF-8 path elements for uploads
+merge_request: 16560
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-parent-preloading.yml b/changelogs/unreleased/bvl-parent-preloading.yml
new file mode 100644
index 00000000000..97c7bbb2a2a
--- /dev/null
+++ b/changelogs/unreleased/bvl-parent-preloading.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issues when rendering groups and their children
+merge_request: 16584
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-redirect-routes-schema.yml b/changelogs/unreleased/fix-redirect-routes-schema.yml
new file mode 100644
index 00000000000..ea2b916307a
--- /dev/null
+++ b/changelogs/unreleased/fix-redirect-routes-schema.yml
@@ -0,0 +1,5 @@
+---
+title: rework indexes on redirect_routes
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/issue_37143_2.yml b/changelogs/unreleased/issue_37143_2.yml
new file mode 100644
index 00000000000..38125f666b2
--- /dev/null
+++ b/changelogs/unreleased/issue_37143_2.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unecessary query from labels filter
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml b/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml
new file mode 100644
index 00000000000..55ab318df7d
--- /dev/null
+++ b/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml
@@ -0,0 +1,6 @@
+---
+title: Ensure that users can reclaim a namespace or project path that is blocked by
+ an orphaned route
+merge_request: 16242
+author:
+type: fixed
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 5f95255334c..2c133f2536d 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -119,7 +119,12 @@ var config = {
{
test: /\_worker\.js$/,
use: [
- { loader: 'worker-loader' },
+ {
+ loader: 'worker-loader',
+ options: {
+ inline: true
+ }
+ },
{ loader: 'babel-loader' },
],
},
diff --git a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb
new file mode 100644
index 00000000000..ab9971be074
--- /dev/null
+++ b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb
@@ -0,0 +1,68 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ReworkRedirectRoutesIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ INDEX_NAME_UNIQUE = "index_redirect_routes_on_path_unique_text_pattern_ops"
+
+ INDEX_NAME_PERM = "index_redirect_routes_on_path_text_pattern_ops_where_permanent"
+ INDEX_NAME_TEMP = "index_redirect_routes_on_path_text_pattern_ops_where_temporary"
+
+ OLD_INDEX_NAME_PATH_TPOPS = "index_redirect_routes_on_path_text_pattern_ops"
+ OLD_INDEX_NAME_PATH_LOWER = "index_on_redirect_routes_lower_path"
+
+ def up
+ disable_statement_timeout
+
+ # this is a plain btree on a single boolean column. It'll never be
+ # selective enough to be valuable. This class is called by
+ # setup_postgresql.rake so it needs to be able to handle this
+ # index not existing.
+ if index_exists?(:redirect_routes, :permanent)
+ remove_concurrent_index(:redirect_routes, :permanent)
+ end
+
+ # If we're on MySQL then the existing index on path is ok. But on
+ # Postgres we need to clean things up:
+ return unless Gitlab::Database.postgresql?
+
+ if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : ""
+
+ # Unique index on lower(path) across both types of redirect_routes:
+ execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);")
+
+ # Make two indexes on path -- one for permanent and one for temporary routes:
+ execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);")
+ execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;")
+
+ # Remove the old indexes:
+
+ # This one needed to be on lower(path) but wasn't so it's replaced with the two above
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};"
+
+ # This one isn't needed because we only ever do = and LIKE on this
+ # column so the varchar_pattern_ops index is sufficient
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};"
+ end
+
+ def down
+ disable_statement_timeout
+
+ add_concurrent_index(:redirect_routes, :permanent)
+
+ return unless Gitlab::Database.postgresql?
+
+ execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);")
+ execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));")
+
+ execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};")
+ execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};")
+ execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};")
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4fb1078ce69..b8a29d8c046 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20171230123729) do
+ActiveRecord::Schema.define(version: 20180113220114) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1535,8 +1535,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do
end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
- add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
- add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t|
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index b86168f935a..0c32afe9046 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -1,5 +1,11 @@
# Fast lookup of authorized SSH keys in the database
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1631) in
+> [GitLab Enterprise Edition Standard](https://about.gitlab.com/gitlab-ee) 9.3.
+>
+> [Available in](https://gitlab.com/gitlab-org/gitlab-ee/issues/3953) GitLab
+> Community Edition 10.4.
+
Regular SSH operations become slow as the number of users grows because OpenSSH
searches for a key to authorize a user via a linear search. In the worst case,
such as when the user is not authorized to access GitLab, OpenSSH will scan the
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index 16ff8d5bb3e..7bf647bbb8b 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -1,11 +1,12 @@
# Dynamic Application Security Testing with GitLab CI/CD
-This example shows how to run
[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_program_analysis)
-on your project's source code by using GitLab CI/CD.
+is using the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
+to perform an analysis on your running web application.
-DAST is using the popular open source tool
-[OWASP ZAProxy](https://github.com/zaproxy/zaproxy) to perform an analysis.
+It can be very useful combined with [Review Apps](../review_apps/index.md).
+
+## Example
All you need is a GitLab Runner with the Docker executor (the shared Runners on
GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`,
@@ -14,22 +15,26 @@ called `dast`:
```yaml
dast:
image: owasp/zap2docker-stable
+ variables:
+ website: "https://example.com"
script:
- mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t https://example.com || true
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
paths: [gl-dast-report.json]
```
-The above example will create a `dast` job in your CI pipeline and will allow
-you to download and analyze the report artifact in JSON format.
+The above example will create a `dast` job in your CI/CD pipeline which will run
+the tests on the URL defined in the `website` variable (change it to use your
+own) and finally write the results in the `gl-dast-report.json` file. You can
+then download and analyze the report artifact in JSON format.
TIP: **Tip:**
Starting with [GitLab Enterprise Edition Ultimate][ee] 10.4, this information will
be automatically extracted and shown right in the merge request widget. To do
so, the CI job must be named `dast` and the artifact path must be
`gl-dast-report.json`.
-[Learn more on dynamic application security testing results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
+[Learn more about DAST results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
[ee]: https://about.gitlab.com/gitlab-ee/
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index df66810a838..03aa6ff8e7c 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -144,6 +144,28 @@ To protect/unprotect Runners:
![specific Runners edit icon](img/protected_runners_check_box.png)
+## Manually clearing the Runners cache
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41249) in GitLab 10.4.
+
+GitLab Runners use [cache](../yaml/README.md#cache) to speed up the execution
+of your jobs by reusing existing data. This however, can sometimes lead to an
+inconsistent behavior.
+
+To start with a fresh copy of the cache, you can easily do it via GitLab's UI:
+
+1. Navigate to your project's **CI/CD > Pipelines** page.
+1. Click on the **Clear Runner caches** to clean up the cache.
+1. On the next push, your CI/CD job will use a new cache.
+
+That way, you don't have to change the [cache key](../yaml/README.md#cache-key)
+in your `.gitlab-ci.yml`.
+
+Behind the scenes, this works by increasing a counter in the database, and the
+value of that counter is used to create the key for the cache. After a push, a
+new key is generated and the old cache is not valid anymore. Eventually, the
+Runner's garbage collector will remove it form the filesystem.
+
## How shared Runners pick jobs
Shared Runners abide to a process queue we call fair usage. The fair usage
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 130f7897b1a..e87b4403854 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -1,26 +1,28 @@
# Connecting GitLab with a Kubernetes cluster
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in GitLab 10.1.
+
+Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes
+cluster in a few steps.
With a cluster associated to your project, you can use Review Apps, deploy your
applications, run your pipelines, and much more, in an easy way.
-Connect your project to Google Kubernetes Engine (GKE) or your own Kubernetes
-cluster in a few steps.
-
-NOTE: **Note:**
-The Cluster integration will eventually supersede the
-[Kubernetes integration](../integrations/kubernetes.md). For the moment,
-you can create only one cluster.
+There are two options when adding a new cluster to your project; either associate
+your account with Google Kubernetes Engine (GKE) so that you can [create new
+clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab,
+or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster).
## Prerequisites
-In order to be able to manage your GKE cluster through GitLab, the following
-prerequisites must be met:
+In order to be able to manage your Kubernetes cluster through GitLab, the
+following prerequisites must be met.
+
+**For a cluster hosted on GKE:**
- The [Google authentication integration](../../../integration/google.md) must
be enabled in GitLab at the instance level. If that's not the case, ask your
- administrator to enable it.
+ GitLab administrator to enable it.
- Your associated Google account must have the right privileges to manage
clusters on GKE. That would mean that a [billing
account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
@@ -31,41 +33,88 @@ prerequisites must be met:
- You must have [Resource Manager
API](https://cloud.google.com/resource-manager/)
-If all of the above requirements are met, you can proceed to add a new GKE
+**For an existing Kubernetes cluster:**
+
+- Since the cluster is already created, there are no prerequisites.
+
+---
+
+If all of the above requirements are met, you can proceed to add a new Kubernetes
cluster.
-## Adding a cluster
+## Adding and creating a new GKE cluster via GitLab
+
+NOTE: **Note:**
+You need Master [permissions] and above to access the Clusters page.
+
+Before proceeding, make sure all [prerequisites](#prerequisites) are met.
+To add a new cluster hosted on GKE to your project:
+
+1. Navigate to your project's **CI/CD > Clusters** page.
+1. Click on **Add cluster**.
+1. Click on **Create with GKE**.
+1. Connect your Google account if you haven't done already by clicking the
+ **Sign in with Google** button.
+1. Fill in the requested values:
+ - **Cluster name** (required) - The name you wish to give the cluster.
+ - **GCP project ID** (required) - The ID of the project you created in your GCP
+ console that will host the Kubernetes cluster. This must **not** be confused
+ with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/)
+ under which the cluster will be created.
+ - **Number of nodes** - The number of nodes you wish the cluster to have.
+ - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
+ of the Virtual Machine instance that the cluster will be based on.
+ - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
+1. Finally, click the **Create cluster** button.
+
+After a few moments, your cluster should be created. If something goes wrong,
+you will be notified.
+
+You can now proceed to install some pre-defined applications and then
+enable the Cluster integration.
+
+## Adding an existing Kubernetes cluster
NOTE: **Note:**
-You need Master [permissions] and above to add a cluster.
-
-There are two options when adding a new cluster; either use Google Kubernetes
-Engine (GKE) or provide the credentials to your own Kubernetes cluster.
-
-To add a new cluster:
-
-1. Navigate to your project's **CI/CD > Cluster** page
-1. If you want to let GitLab create a cluster on GKE for you, go through the
- following steps, otherwise skip to the next one.
- 1. Click on **Create with GKE**
- 1. Connect your Google account if you haven't done already by clicking the
- **Sign in with Google** button
- 1. Fill in the requested values:
- - **Cluster name** (required) - The name you wish to give the cluster.
- - **GCP project ID** (required) - The ID of the project you created in your GCP
- console that will host the Kubernetes cluster. This must **not** be confused
- with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
- - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/)
- under which the cluster will be created.
- - **Number of nodes** - The number of nodes you wish the cluster to have.
- - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
- of the Virtual Machine instance that the cluster will be based on.
- - **Project namespace** - The unique namespace for this project. By default you
- don't have to fill it in; by leaving it blank, GitLab will create one for you.
-1. If you want to use your own existing Kubernetes cluster, click on
- **Add an existing cluster** and fill in the details as described in the
- [Kubernetes integration](../integrations/kubernetes.md) documentation.
-1. Finally, click the **Create cluster** button
+You need Master [permissions] and above to access the Clusters page.
+
+To add an existing Kubernetes cluster to your project:
+
+1. Navigate to your project's **CI/CD > Clusters** page.
+1. Click on **Add cluster**.
+1. Click on **Add an existing cluster** and fill in the details:
+ - **Cluster name** (required) - The name you wish to give the cluster.
+ - **Environment scope** (required)- The
+ [associated environment](#setting-the-environment-scope) to this cluster.
+ - **API URL** (required) -
+ It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
+ exposes several APIs, we want the "base" URL that is common to all of them,
+ e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+ - **CA certificate** (optional) -
+ If the API is using a self-signed TLS certificate, you'll also need to include
+ the `ca.crt` contents here.
+ - **Token** -
+ GitLab authenticates against Kubernetes using service tokens, which are
+ scoped to a particular `namespace`. If you don't have a service token yet,
+ you can follow the
+ [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
+ to create one. You can also view or create service tokens in the
+ [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
+ (under **Config > Secrets**).
+ - **Project namespace** (optional) - The following apply:
+ - By default you don't have to fill it in; by leaving it blank, GitLab will
+ create one for you.
+ - Each project should have a unique namespace.
+ - The project namespace is not necessarily the namespace of the secret, if
+ you're using a secret with broader permissions, like the secret from `default`.
+ - You should **not** use `default` as the project namespace.
+ - If you or someone created a secret specifically for the project, usually
+ with limited permissions, the secret's namespace and project namespace may
+ be the same.
+1. Finally, click the **Create cluster** button.
+
+The Kubernetes service takes the following parameters:
After a few moments, your cluster should be created. If something goes wrong,
you will be notified.
@@ -85,6 +134,91 @@ added directly to your configured cluster. Those applications are needed for
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
+## Setting the environment scope
+
+When adding more than one clusters, you need to differentiate them with an
+environment scope. The environment scope associates clusters and
+[environments](../../../ci/environments.md) in an 1:1 relationship similar to how the
+[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables)
+work.
+
+The default environment scope is `*`, which means all jobs, regardless of their
+environment, will use that cluster. Each scope can only be used by a single
+cluster in a project, and a validation error will occur if otherwise.
+
+---
+
+For example, let's say the following clusters exist in a project:
+
+| Cluster | Environment scope |
+| ---------- | ------------------- |
+| Development| `*` |
+| Staging | `staging/*` |
+| Production | `production/*` |
+
+And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md):
+
+```yaml
+stages:
+- test
+- deploy
+
+test:
+ stage: test
+ script: sh test
+
+deploy to staging:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: staging/$CI_COMMIT_REF_NAME
+ url: https://staging.example.com/
+
+deploy to production:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: production/$CI_COMMIT_REF_NAME
+ url: https://example.com/
+```
+
+The result will then be:
+
+- The development cluster will be used for the "test" job.
+- The staging cluster will be used for the "deploy to staging" job.
+- The production cluster will be used for the "deploy to production" job.
+
+## Multiple Kubernetes clusters
+
+> Introduced in [GitLab Enterprise Edition Premium][ee] 10.3.
+
+With GitLab EEP, you can associate more than one Kubernetes clusters to your
+project. That way you can have different clusters for different environments,
+like dev, staging, production, etc.
+
+To add another cluster, follow the same steps as described in [adding a
+Kubernetes cluster](#adding-a-kubernetes-cluster) and make sure to
+[set an environment scope](#setting-the-environment-scope) that will
+differentiate the new cluster with the rest.
+
+## Deployment variables
+
+The Kubernetes cluster integration exposes the following
+[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+GitLab CI/CD build environment:
+
+- `KUBE_URL` - Equal to the API URL.
+- `KUBE_TOKEN` - The Kubernetes token.
+- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
+ The default value is `<project_name>-<project_id>`. You can overwrite it to
+ use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
+ receive the default value.
+- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
+ to a file containing PEM data.
+- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
+- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
+ CA bundle would be embedded if specified.
+
## Enabling or disabling the Cluster integration
After you have successfully added your cluster information, you can enable the
@@ -111,4 +245,62 @@ To remove the Cluster integration from your project, simply click on the
**Remove integration** button. You will then be able to follow the procedure
and [add a cluster](#adding-a-cluster) again.
+## What you can get with the Kubernetes integration
+
+Here's what you can do with GitLab if you enable the Kubernetes integration.
+
+### Deploy Boards (EEP)
+
+> Available in [GitLab Enterprise Edition Premium][ee].
+
+GitLab's Deploy Boards offer a consolidated view of the current health and
+status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
+displaying the status of the pods in the deployment. Developers and other
+teammates can view the progress and status of a rollout, pod by pod, in the
+workflow they already use without any need to access Kubernetes.
+
+[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
+
+### Canary Deployments (EEP)
+
+> Available in [GitLab Enterprise Edition Premium][ee].
+
+Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
+and visualize your canary deployments right inside the Deploy Board, without
+the need to leave GitLab.
+
+[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
+
+### Kubernetes monitoring
+
+Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
+[NGINX ingress](../integrations/prometheus_library/nginx.md) is also supported.
+
+[> Read more about Kubernetes monitoring](../integrations/prometheus_library/kubernetes.md)
+
+### Auto DevOps
+
+Auto DevOps automatically detects, builds, tests, deploys, and monitors your
+applications.
+
+To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
+you will need the Kubernetes project integration enabled.
+
+[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
+
+### Web terminals
+
+NOTE: **Note:**
+Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is limited to the first container in the
+first pod of your environment.
+
+When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
+support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
+Docker and Kubernetes, so you get a new shell session within your existing
+containers. To use this integration, you should deploy to Kubernetes using
+the deployment variables above, ensuring any pods you create are labelled with
+`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+
[permissions]: ../../permissions.md
+[ee]: https://about.gitlab.com/gitlab-ee/
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index 710cf78e84f..543baaa81e1 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -2,11 +2,15 @@
last_updated: 2017-12-28
---
-CAUTION: **Warning:**
-Kubernetes service integration has been deprecated in GitLab 10.3. If the service is active the cluster information still be editable, however we advised to disable and reconfigure the clusters using the new [Clusters](../clusters/index.md) page. If the service is inactive the fields will be uneditable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
-
# GitLab Kubernetes / OpenShift integration
+CAUTION: **Warning:**
+The Kubernetes service integration has been deprecated in GitLab 10.3. If the
+service is active, the cluster information will still be editable, however we
+advise to disable and reconfigure the clusters using the new
+[Clusters](../clusters/index.md) page. If the service is inactive, the fields
+will not be editable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
+
GitLab can be configured to interact with Kubernetes, or other systems using the
Kubernetes API (such as OpenShift).
diff --git a/doc/user/project/pipelines/img/pipeline_schedule_play.png b/doc/user/project/pipelines/img/pipeline_schedule_play.png
new file mode 100644
index 00000000000..f594ceee19d
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipeline_schedule_play.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipeline_schedules_list.png b/doc/user/project/pipelines/img/pipeline_schedules_list.png
index 50d9d184b05..2ab2061db94 100644
--- a/doc/user/project/pipelines/img/pipeline_schedules_list.png
+++ b/doc/user/project/pipelines/img/pipeline_schedules_list.png
Binary files differ
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 2101e3b1d58..34809a2826f 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -31,6 +31,20 @@ is installed on.
![Schedules list](img/pipeline_schedules_list.png)
+### Running a scheduled pipeline manually
+
+> [Introduced][ce-15700] in GitLab 10.4.
+
+To trigger a pipeline schedule manually, click the "Play" button:
+
+![Play Pipeline Schedule](img/pipeline_schedule_play.png)
+
+This will schedule a background job to run the pipeline schedule. A flash
+message will provide a link to the CI/CD Pipeline index page.
+
+To help avoid abuse, users are rate limited to triggering a pipeline once per
+minute.
+
### Making use of scheduled pipeline variables
> [Introduced][ce-12328] in GitLab 9.4.
@@ -90,4 +104,5 @@ don't have admin access to the server, ask your administrator.
[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853
[ce-12328]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12328
+[ce-15700]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15700
[settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index f6169b2c85d..9bdedeb6615 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -50,7 +50,7 @@ module Banzai
end
def process_link_to_upload_attr(html_attr)
- path_parts = [html_attr.value]
+ path_parts = [Addressable::URI.unescape(html_attr.value)]
if group
path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
@@ -58,13 +58,13 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path)
end
- path = File.join(*path_parts)
+ path = Addressable::URI.escape(File.join(*path_parts))
html_attr.value =
if context[:only_path]
path
else
- URI.join(Gitlab.config.gitlab.base_url, path).to_s
+ Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 031fccba92b..f421bf69e8f 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -132,6 +132,8 @@ module Gitlab
end
def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
+ return unless path
+
path = path.sub(/\A\/*/, '')
path = '/' if path.empty?
name = File.basename(path)
@@ -173,6 +175,8 @@ module Gitlab
end
def find_by_rugged(repository, sha, path, limit:)
+ return unless path
+
rugged_commit = repository.lookup(sha)
root_tree = rugged_commit.tree
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index c9e3eed82f2..c996537cfbe 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -7,6 +7,7 @@ require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
+require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
@@ -17,4 +18,5 @@ task setup_postgresql: :environment do
AddLowerPathIndexToRedirectRoutes.new.up
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
+ ReworkRedirectRoutesIndexes.new.up
end
diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb
index fb9d3efbac0..7f2eaf95165 100644
--- a/spec/controllers/dashboard/groups_controller_spec.rb
+++ b/spec/controllers/dashboard/groups_controller_spec.rb
@@ -20,4 +20,24 @@ describe Dashboard::GroupsController do
expect(assigns(:groups)).to contain_exactly(member_of_group)
end
+
+ context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
+ let!(:top_level_result) { create(:group, name: 'chef-top') }
+ let!(:top_level_a) { create(:group, name: 'top-a') }
+ let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
+ let!(:other_group) { create(:group, name: 'other') }
+
+ before do
+ top_level_result.add_master(user)
+ top_level_a.add_master(user)
+ end
+
+ it 'renders only groups the user is a member of when searching hierarchy correctly' do
+ get :index, filter: 'chef', format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ all_groups = [top_level_result, top_level_a, sub_level_result_a]
+ expect(assigns(:groups)).to contain_exactly(*all_groups)
+ end
+ end
end
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index cb1b460fc0e..22d3076c269 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -160,6 +160,30 @@ describe Groups::ChildrenController do
expect(json_response).to eq([])
end
+ it 'succeeds if multiple pages contain matching subgroups' do
+ create(:group, parent: group, name: 'subgroup-filter-1')
+ create(:group, parent: group, name: 'subgroup-filter-2')
+
+ # Creating the group-to-nest first so it would be loaded into the
+ # relation first before it's parents, this is what would cause the
+ # crash in: https://gitlab.com/gitlab-org/gitlab-ce/issues/40785.
+ #
+ # If we create the parent groups first, those would be loaded into the
+ # collection first, and the pagination would cut off the actual search
+ # result. In this case the hierarchy can be rendered without crashing,
+ # it's just incomplete.
+ group_to_nest = create(:group, parent: group, name: 'subsubgroup-filter-3')
+ subgroup = create(:group, parent: group)
+ 3.times do |i|
+ subgroup = create(:group, parent: subgroup)
+ end
+ group_to_nest.update!(parent: subgroup)
+
+ get :index, group_id: group.to_param, filter: 'filter', per_page: 3, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
it 'includes pagination headers' do
2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") }
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 3bbe168f6d5..6a41c4d23ea 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::AvatarsController do
- let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project) { create(:project, :repository, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:user) { create(:user) }
before do
@@ -10,6 +10,12 @@ describe Projects::AvatarsController do
controller.instance_variable_set(:@project, project)
end
+ it 'GET #show' do
+ get :show, namespace_id: project.namespace.id, project_id: project.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
it 'removes avatar from DB by calling destroy' do
delete :destroy, namespace_id: project.namespace.id, project_id: project.id
expect(project.avatar.present?).to be_falsey
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 53706ef84bc..c7cfd01f588 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -34,6 +34,9 @@ describe 'New issue', :js do
click_button 'Submit issue'
+ # reCAPTCHA alerts when it can't contact the server, so just accept it and move on
+ page.driver.browser.switch_to.alert.accept
+
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a5cd858b11a..e661db1809a 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -384,12 +384,12 @@ feature 'Jobs' do
expect(page).to have_link('Trigger this manual action')
end
- it 'plays manual action', :js do
+ it 'plays manual action and shows pending status', :js do
click_link 'Trigger this manual action'
wait_for_requests
- expect(page).to have_content('This job has not been triggered')
- expect(page).to have_content('This job is stuck, because the project doesn\'t have any runners online assigned to it.')
+ expect(page).to have_content('This job has not started yet')
+ expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner')
expect(page).to have_content('pending')
end
end
@@ -403,6 +403,20 @@ feature 'Jobs' do
it 'shows empty state' do
expect(page).to have_content('This job has not been triggered yet')
+ expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered')
+ end
+ end
+
+ context 'Pending job' do
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline) }
+
+ before do
+ visit project_job_path(project, job)
+ end
+
+ it 'shows pending empty state' do
+ expect(page).to have_content('This job has not started yet')
+ expect(page).to have_content('This job is in pending state and is waiting to be picked by a runner')
end
end
end
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index ae050f36b4a..375bcc9087e 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -35,6 +35,15 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
+ it 'does not include projects shared with the group' do
+ project = create(:project, namespace: group)
+ other_project = create(:project)
+ other_project.project_group_links.create(group: group,
+ group_access: ProjectGroupLink::MASTER)
+
+ expect(finder.execute).to contain_exactly(project)
+ end
+
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
@@ -189,6 +198,17 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(subgroup, matching_project)
end
+ context 'with a small page size' do
+ let(:params) { { filter: 'test', per_page: 1 } }
+
+ it 'contains all the ancestors of a matching subgroup regardless the page size' do
+ subgroup = create(:group, :private, parent: group)
+ matching = create(:group, :private, name: 'testgroup', parent: subgroup)
+
+ expect(finder.execute).to contain_exactly(subgroup, matching)
+ end
+ end
+
it 'does not include the parent itself' do
group.update!(name: 'test')
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index d601cbdb39b..c8d64d64cf4 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -192,4 +192,33 @@ describe IssuablesHelper do
expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
end
end
+
+ describe '#selected_labels' do
+ context 'if label_name param is a string' do
+ it 'returns a new label with title' do
+ allow(helper).to receive(:params)
+ .and_return(ActionController::Parameters.new(label_name: 'test label'))
+
+ labels = helper.selected_labels
+
+ expect(labels).to be_an(Array)
+ expect(labels.size).to eq(1)
+ expect(labels.first.title).to eq('test label')
+ end
+ end
+
+ context 'if label_name param is an array' do
+ it 'returns a new label with title for each element' do
+ allow(helper).to receive(:params)
+ .and_return(ActionController::Parameters.new(label_name: ['test label 1', 'test label 2']))
+
+ labels = helper.selected_labels
+
+ expect(labels).to be_an(Array)
+ expect(labels.size).to eq(2)
+ expect(labels.first.title).to eq('test label 1')
+ expect(labels.second.title).to eq('test label 2')
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ede9d232efd..c0251bf7dc0 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ProjectsHelper do
+ include ProjectForksHelper
+
describe "#project_status_css_class" do
it "returns appropriate class" do
expect(project_status_css_class("started")).to eq("active")
@@ -10,9 +12,9 @@ describe ProjectsHelper do
end
describe "can_change_visibility_level?" do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project) }
let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
- let(:fork_project) { Projects::ForkService.new(project, user).execute }
+ let(:forked_project) { fork_project(project, user) }
it "returns false if there are no appropriate permissions" do
allow(helper).to receive(:can?) { false }
@@ -26,21 +28,29 @@ describe ProjectsHelper do
expect(helper.can_change_visibility_level?(project, user)).to be_truthy
end
+ it 'allows visibility level to be changed if the project is forked' do
+ allow(helper).to receive(:can?).with(user, :change_visibility_level, project) { true }
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ fork_project(project)
+
+ expect(helper.can_change_visibility_level?(project, user)).to be_truthy
+ end
+
context "forks" do
it "returns false if there are permissions and origin project is PRIVATE" do
allow(helper).to receive(:can?) { true }
- project.update visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- expect(helper.can_change_visibility_level?(fork_project, user)).to be_falsey
+ expect(helper.can_change_visibility_level?(forked_project, user)).to be_falsey
end
it "returns true if there are permissions and origin project is INTERNAL" do
allow(helper).to receive(:can?) { true }
- project.update visibility_level: Gitlab::VisibilityLevel::INTERNAL
+ project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- expect(helper.can_change_visibility_level?(fork_project, user)).to be_truthy
+ expect(helper.can_change_visibility_level?(forked_project, user)).to be_truthy
end
end
end
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
index 97e3ab682c5..7198dbd4cf2 100644
--- a/spec/javascripts/flash_spec.js
+++ b/spec/javascripts/flash_spec.js
@@ -183,11 +183,15 @@ describe('Flash', () => {
});
it('adds flash element into container', () => {
- flash('test');
+ flash('test', 'alert', document, null, false, true);
expect(
document.querySelector('.flash-alert'),
).not.toBeNull();
+
+ expect(
+ document.body.className,
+ ).toContain('flash-shown');
});
it('adds flash into specified parent', () => {
@@ -220,13 +224,17 @@ describe('Flash', () => {
});
it('removes element after clicking', () => {
- flash('test', 'alert', document, null, false);
+ flash('test', 'alert', document, null, false, true);
document.querySelector('.flash-alert').click();
expect(
document.querySelector('.flash-alert'),
).toBeNull();
+
+ expect(
+ document.body.className,
+ ).not.toContain('flash-shown');
});
describe('with actionConfig', () => {
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 1454ca52018..9280db072b3 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -218,6 +218,39 @@ describe('Issuable output', () => {
});
});
+ describe('shows dialog when issue has unsaved changed', () => {
+ it('confirms on title change', (done) => {
+ vm.showForm = true;
+ vm.state.titleText = 'title has changed';
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ Vue.nextTick(() => {
+ expect(e.returnValue).not.toBeNull();
+ done();
+ });
+ });
+
+ it('confirms on description change', (done) => {
+ vm.showForm = true;
+ vm.state.descriptionText = 'description has changed';
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ Vue.nextTick(() => {
+ expect(e.returnValue).not.toBeNull();
+ done();
+ });
+ });
+
+ it('does nothing when nothing has changed', (done) => {
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ Vue.nextTick(() => {
+ expect(e.returnValue).toBeNull();
+ done();
+ });
+ });
+ });
+
describe('error when updating', () => {
beforeEach(() => {
spyOn(window, 'Flash').and.callThrough();
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 6f8dad6b835..69a23d7b2f3 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -63,13 +63,13 @@ describe('text_utility', () => {
});
});
- describe('stripeHtml', () => {
+ describe('stripHtml', () => {
it('replaces html tag with the default replacement', () => {
- expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
+ expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
});
it('replaces html tags with the provided replacement', () => {
- expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
+ expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
});
});
});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
index 0b0d34f072a..853ef7f3224 100644
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -300,19 +300,6 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
- it('closes all files', (done) => {
- store.state.openFiles.push(file());
- store.state.openFiles[0].opened = true;
-
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(Vue.nextTick)
- .then(() => {
- expect(store.state.openFiles.length).toBe(0);
-
- done();
- }).catch(done.fail);
- });
-
it('scrolls to top of page', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 9230b5874df..073f26cc78f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -170,14 +170,14 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.iconClass).toEqual('success');
});
- it('shows x for failed status', () => {
+ it('shows warning icon for failed status', () => {
vm.mr.hasCI = true;
- expect(vm.iconClass).toEqual('failed');
+ expect(vm.iconClass).toEqual('warning');
});
- it('shows x for merge not allowed', () => {
+ it('shows warning icon for merge not allowed', () => {
vm.mr.hasCI = true;
- expect(vm.iconClass).toEqual('failed');
+ expect(vm.iconClass).toEqual('warning');
});
});
diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js
index 1baf3537741..5cd3466f501 100644
--- a/spec/javascripts/vue_shared/components/loading_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_icon_spec.js
@@ -16,7 +16,8 @@ describe('Loading Icon Component', () => {
).toEqual('fa fa-spin fa-spinner fa-1x');
expect(component.$el.tagName).toEqual('DIV');
- expect(component.$el.classList.contains('text-center')).toEqual(true);
+ expect(component.$el.classList).toContain('text-center');
+ expect(component.$el.classList).toContain('loading-container');
});
it('should render accessibility attributes', () => {
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 7e17457ce70..3ca4652f7cc 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -278,18 +278,19 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
end
- it 'supports Unicode filenames' do
+ it 'supports unescaped Unicode filenames' do
path = '/uploads/한글.png'
- escaped = Addressable::URI.escape(path)
+ doc = filter(link(path))
- # Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class)
- .to receive(:file_exists?).and_return(true)
- allow_any_instance_of(described_class)
- .to receive(:image?).with(path).and_return(true)
+ expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
+ end
+ it 'supports escaped Unicode filenames' do
+ path = '/uploads/한글.png'
+ escaped = Addressable::URI.escape(path)
doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match "/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png"
+
+ expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 07eb5b82d5f..8706c89c147 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -16,6 +16,18 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
shared_examples 'finding blobs' do
+ context 'nil path' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
+
+ it { expect(blob).to eq(nil) }
+ end
+
+ context 'blank path' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') }
+
+ it { expect(blob).to eq(nil) }
+ end
+
context 'file in subdir' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 00afa09f1a3..6f24dd6811e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1952,6 +1952,10 @@ describe Project do
expect(second_fork.fork_source).to eq(project)
end
+
+ it 'returns nil if it is the root of the fork network' do
+ expect(project.fork_source).to be_nil
+ end
end
describe '#lfs_storage_project' do
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 8a3b1034f3c..88f54fd10e5 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -79,6 +79,13 @@ describe Route do
end
describe 'callbacks' do
+ context 'before validation' do
+ it 'calls #delete_conflicting_orphaned_routes' do
+ expect(route).to receive(:delete_conflicting_orphaned_routes)
+ route.valid?
+ end
+ end
+
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
@@ -378,4 +385,58 @@ describe Route do
end
end
end
+
+ describe '#delete_conflicting_orphaned_routes' do
+ context 'when there is a conflicting route' do
+ let!(:conflicting_group) { create(:group, path: 'foo') }
+
+ before do
+ route.path = conflicting_group.route.path
+ end
+
+ context 'when the route is orphaned' do
+ let!(:offending_route) { conflicting_group.route }
+
+ before do
+ Group.delete(conflicting_group) # Orphan the route
+ end
+
+ it 'deletes the orphaned route' do
+ expect do
+ route.valid?
+ end.to change { described_class.count }.from(2).to(1)
+ end
+
+ it 'passes validation, as usual' do
+ expect(route.valid?).to be_truthy
+ end
+ end
+
+ context 'when the route is not orphaned' do
+ it 'does not delete the conflicting route' do
+ expect do
+ route.valid?
+ end.not_to change { described_class.count }
+ end
+
+ it 'fails validation, as usual' do
+ expect(route.valid?).to be_falsey
+ end
+ end
+ end
+
+ context 'when there are no conflicting routes' do
+ it 'does not delete any routes' do
+ route
+
+ expect do
+ route.valid?
+ end.not_to change { described_class.count }
+ end
+
+ it 'passes validation, as usual' do
+ expect(route.valid?).to be_truthy
+ end
+ end
+ end
end
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index eec356b9f47..7046932342b 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -462,6 +462,8 @@ production:
}
function create_secret() {
+ echo "Create secret..."
+
kubectl create secret -n "$KUBE_NAMESPACE" \
docker-registry gitlab-registry \
--docker-server="$CI_REGISTRY" \