BigW Consortium Gitlab

Commit ece43ee6 by Luke Bennett

Merge branch '10-4-stable-patch-1' into '10-4-stable'

Prepare 10.4.1 release See merge request gitlab-org/gitlab-ce!16629
parents 128ef10b 668d81a0
......@@ -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);
},
);
......
......@@ -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;
};
......
......@@ -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"
......
......@@ -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;
......
......@@ -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"
>
......
......@@ -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;
});
}
......
......@@ -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() {
......
......@@ -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 },
......
......@@ -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 {
......
......@@ -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,
......
......@@ -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 {
......
......@@ -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 = (
......
......@@ -64,7 +64,7 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
content: file.raw,
changed: false,
});
},
......
......@@ -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);
......
......@@ -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);
......@@ -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>
......
......@@ -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"
......
......@@ -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
......
......@@ -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
......
......@@ -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">
......
......@@ -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
......
......@@ -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
......
......@@ -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';
},
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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"
......
......@@ -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
......
......@@ -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});
}
}
}
......
......@@ -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
......
......@@ -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
......
......@@ -71,7 +71,7 @@ class LabelsFinder < UnionFinder
end
def projects?
params[:project_ids].present?
params[:project_ids]
end
def only_group_labels?
......
......@@ -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?
......
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
......
......@@ -1023,6 +1023,8 @@ class Project < ActiveRecord::Base
end
def fork_source
return nil unless forked?
forked_from_project || fork_network&.root_project
end
......
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
- @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
......
!!! 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
- 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
......@@ -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 }
......
......@@ -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
......
---
title: Fix bug in which projects with forks could not change visibility settings from
Private to Public
merge_request: 16595
author:
type: fixed
---
title: Correctly escape UTF-8 path elements for uploads
merge_request: 16560
author:
type: fixed
---
title: Fix issues when rendering groups and their children
merge_request: 16584
author:
type: fixed
---
title: rework indexes on redirect_routes
merge_request:
author:
type: performance
---
title: Remove unecessary query from labels filter
merge_request:
author:
type: performance
---
title: Ensure that users can reclaim a namespace or project path that is blocked by
an orphaned route
merge_request: 16242
author:
type: fixed
......@@ -119,7 +119,12 @@ var config = {
{
test: /\_worker\.js$/,
use: [
{ loader: 'worker-loader' },
{
loader: 'worker-loader',
options: {
inline: true
}
},
{ loader: 'babel-loader' },
],
},
......
# 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
......@@ -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|
......
# 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
......
# 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/
......@@ -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
......
# 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/
......@@ -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).
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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}") }
......
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
......
......@@ -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')
......
......@@ -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
......
......@@ -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')
......
......@@ -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
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
......
......@@ -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', () => {
......
......@@ -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();
......
......@@ -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 .');
});
});
});
......@@ -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(() => {
......
......@@ -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');
});
});
......
......@@ -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', () => {
......
......@@ -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
......
......@@ -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") }
......
......@@ -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
......
......@@ -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
......@@ -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" \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment