summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/dag/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue61
-rw-r--r--app/assets/javascripts/pipelines/components/dag/interactions.js134
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue70
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue67
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue30
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js11
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue184
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss2
-rw-r--r--app/controllers/concerns/issuable_actions.rb29
-rw-r--r--app/controllers/projects/ci/daily_build_group_report_results_controller.rb2
-rw-r--r--app/controllers/projects/graphs_controller.rb5
-rw-r--r--app/finders/ci/daily_build_group_report_results_finder.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_labels.rb5
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/event.rb9
-rw-r--r--app/models/internal_id.rb24
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/project_ci_cd_setting.rb13
-rw-r--r--app/models/user_interacted_project.rb14
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/services/ci/extract_sections_from_build_trace_service.rb2
-rw-r--r--app/services/clusters/applications/prometheus_config_service.rb12
-rw-r--r--app/services/issuable/bulk_update_service.rb5
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb2
-rw-r--r--app/services/issuable_base_service.rb10
-rw-r--r--app/services/projects/detect_repository_languages_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/suggestions/create_service.rb2
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml4
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml6
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb4
-rw-r--r--changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml5
-rw-r--r--changelogs/unreleased/220144-substitute-variables-in-alerts.yml5
-rw-r--r--changelogs/unreleased/bump_ci_auto_deploy_0_16.yml5
-rw-r--r--changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml5
-rw-r--r--changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml5
-rw-r--r--changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml5
-rw-r--r--changelogs/unreleased/sh-avoid-extra-route-reload.yml5
-rw-r--r--changelogs/unreleased/sh-workhorse-direct-access-upload.yml5
-rw-r--r--changelogs/unreleased/templates-current-folder-fix.yml5
-rw-r--r--config/application.rb5
-rw-r--r--config/initializers/8_devise.rb5
-rw-r--r--db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb2
-rw-r--r--doc/administration/gitaly/praefect.md35
-rw-r--r--doc/administration/object_storage.md84
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md33
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/topics/autodevops/customize.md1
-rw-r--r--doc/user/profile/personal_access_tokens.md52
-rw-r--r--doc/user/project/import/jira.md1
-rw-r--r--doc/user/project/service_desk.md4
-rw-r--r--lib/gitlab/background_migration/backfill_project_repositories.rb2
-rw-r--r--lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb2
-rw-r--r--lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb2
-rw-r--r--lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb2
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb2
-rw-r--r--lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/experimentation.rb6
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb2
-rw-r--r--lib/gitlab/github_import/importer/diff_note_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/label_links_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/note_importer.rb2
-rw-r--r--lib/gitlab/import/database_helpers.rb2
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/object_storage/direct_upload.rb28
-rw-r--r--locale/gitlab.pot18
-rw-r--r--rubocop/cop/gitlab/bulk_insert.rb23
-rw-r--r--spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb45
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb44
-rw-r--r--spec/factories/design_management/designs.rb2
-rw-r--r--spec/finders/ci/daily_build_group_report_results_finder_spec.rb26
-rw-r--r--spec/fixtures/packages/conan/package_files/conan_package.tgzbin0 -> 2125 bytes
-rw-r--r--spec/fixtures/packages/conan/package_files/conaninfo.txt33
-rw-r--r--spec/fixtures/packages/conan/package_files/conanmanifest.txt4
-rw-r--r--spec/fixtures/packages/conan/recipe_files/conanfile.py47
-rw-r--r--spec/fixtures/packages/conan/recipe_files/conanmanifest.txt2
-rw-r--r--spec/fixtures/packages/maven/maven-metadata.xml25
-rw-r--r--spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jarbin0 -> 2526 bytes
-rw-r--r--spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom34
-rw-r--r--spec/fixtures/packages/npm/foo-1.0.1.tgzbin0 -> 134 bytes
-rw-r--r--spec/fixtures/packages/npm/payload.json30
-rw-r--r--spec/fixtures/packages/npm/payload_with_duplicated_packages.json44
-rw-r--r--spec/fixtures/packages/nuget/package.nupkgbin0 -> 3513 bytes
-rw-r--r--spec/fixtures/packages/nuget/with_dependencies.nuspec19
-rw-r--r--spec/fixtures/packages/nuget/with_metadata.nuspec19
-rw-r--r--spec/fixtures/packages/pypi/sample-project.tar.gzbin0 -> 1149 bytes
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js40
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js125
-rw-r--r--spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js111
-rw-r--r--spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js74
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js27
-rw-r--r--spec/frontend/registry/explorer/mock_data.js4
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js292
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb3
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb117
-rw-r--r--spec/models/event_spec.rb9
-rw-r--r--spec/models/internal_id_spec.rb41
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb19
-rw-r--r--spec/models/user_interacted_project_spec.rb15
-rw-r--r--spec/policies/project_policy_spec.rb59
-rw-r--r--spec/rubocop/cop/gitlab/bulk_insert_spec.rb19
-rw-r--r--spec/services/clusters/applications/prometheus_config_service_spec.rb16
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb89
-rw-r--r--spec/services/issues/update_service_spec.rb34
-rw-r--r--spec/views/projects/issues/import_csv/_button.html.haml_spec.rb43
-rw-r--r--vendor/project_templates/learn_gitlab.tar.gzbin0 -> 229916 bytes
116 files changed, 1834 insertions, 800 deletions
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ceaf7058c5a..586d6867ab4 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -4,7 +4,7 @@ import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { GlModal } from '@gitlab/ui';
import { modalTypes } from '../../constants';
-import { trimPathComponents } from '../../utils';
+import { trimPathComponents, getPathParent } from '../../utils';
export default {
components: {
@@ -85,8 +85,10 @@ export default {
}
},
createFromTemplate(template) {
+ const parent = getPathParent(this.entryName);
+ const name = parent ? `${parent}/${template.name}` : template.name;
this.createTempEntry({
- name: template.name,
+ name,
type: this.modalType,
});
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/pipelines/components/dag/constants.js
index 252d2d8e4ad..51b1fb4f4cc 100644
--- a/app/assets/javascripts/pipelines/components/dag/constants.js
+++ b/app/assets/javascripts/pipelines/components/dag/constants.js
@@ -1,4 +1,10 @@
+/* Error constants */
export const PARSE_FAILURE = 'parse_failure';
export const LOAD_FAILURE = 'load_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const DEFAULT = 'default';
+
+/* Interaction handles */
+export const IS_HIGHLIGHTED = 'dag-highlighted';
+export const LINK_SELECTOR = 'dag-link';
+export const NODE_SELECTOR = 'dag-node';
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index d91fe9bb7f4..063ec091e4d 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -1,8 +1,13 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
-import { PARSE_FAILURE } from './constants';
-
+import { LINK_SELECTOR, NODE_SELECTOR, PARSE_FAILURE } from './constants';
+import {
+ highlightLinks,
+ restoreLinks,
+ toggleLinkHighlight,
+ togglePathHighlights,
+} from './interactions';
import { getMaxNodes, removeOrphanNodes } from './parsing_utils';
import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
@@ -16,11 +21,7 @@ export default {
paddingForLabels: 100,
labelMargin: 8,
- // can plausibly applied through CSS instead, TBD
baseOpacity: 0.8,
- highlightIn: 1,
- highlightOut: 0.2,
-
containerClasses: ['dag-graph-container', 'gl-display-flex', 'gl-flex-direction-column'].join(
' ',
),
@@ -88,6 +89,20 @@ export default {
);
},
+ appendLinkInteractions(link) {
+ return link
+ .on('mouseover', highlightLinks)
+ .on('mouseout', restoreLinks.bind(null, this.$options.viewOptions.baseOpacity))
+ .on('click', toggleLinkHighlight.bind(null, this.$options.viewOptions.baseOpacity));
+ },
+
+ appendNodeInteractions(node) {
+ return node.on(
+ 'click',
+ togglePathHighlights.bind(null, this.$options.viewOptions.baseOpacity),
+ );
+ },
+
appendLabelAsForeignObject(d, i, n) {
const currentNode = n[i];
const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, {
@@ -163,15 +178,17 @@ export default {
},
createLinks(svg, linksData) {
- const link = this.generateLinks(svg, linksData);
- this.createGradient(link);
- this.createClip(link);
- this.appendLinks(link);
+ const links = this.generateLinks(svg, linksData);
+ this.createGradient(links);
+ this.createClip(links);
+ this.appendLinks(links);
+ this.appendLinkInteractions(links);
},
createNodes(svg, nodeData) {
- this.generateNodes(svg, nodeData);
+ const nodes = this.generateNodes(svg, nodeData);
this.labelNodes(svg, nodeData);
+ this.appendNodeInteractions(nodes);
},
drawGraph({ maxNodesPerLayer, linksAndNodes }) {
@@ -202,37 +219,39 @@ export default {
},
generateLinks(svg, linksData) {
- const linkContainerName = 'dag-link';
-
return svg
.append('g')
.attr('fill', 'none')
.attr('stroke-opacity', this.$options.viewOptions.baseOpacity)
- .selectAll(`.${linkContainerName}`)
+ .selectAll(`.${LINK_SELECTOR}`)
.data(linksData)
.enter()
.append('g')
.attr('id', d => {
- return this.createAndAssignId(d, 'uid', linkContainerName);
+ return this.createAndAssignId(d, 'uid', LINK_SELECTOR);
})
- .classed(`${linkContainerName} gl-cursor-pointer`, true);
+ .classed(`${LINK_SELECTOR} gl-cursor-pointer`, true);
},
generateNodes(svg, nodeData) {
- const nodeContainerName = 'dag-node';
const { nodeWidth } = this.$options.viewOptions;
return svg
.append('g')
- .selectAll(`.${nodeContainerName}`)
+ .selectAll(`.${NODE_SELECTOR}`)
.data(nodeData)
.enter()
.append('line')
- .classed(`${nodeContainerName} gl-cursor-pointer`, true)
+ .classed(`${NODE_SELECTOR} gl-cursor-pointer`, true)
.attr('id', d => {
- return this.createAndAssignId(d, 'uid', nodeContainerName);
+ return this.createAndAssignId(d, 'uid', NODE_SELECTOR);
+ })
+ .attr('stroke', d => {
+ const color = this.color(d);
+ /* eslint-disable-next-line no-param-reassign */
+ d.color = color;
+ return color;
})
- .attr('stroke', this.color)
.attr('stroke-width', nodeWidth)
.attr('stroke-linecap', 'round')
.attr('x1', d => Math.floor((d.x1 + d.x0) / 2))
diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/pipelines/components/dag/interactions.js
new file mode 100644
index 00000000000..c9008730c90
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/dag/interactions.js
@@ -0,0 +1,134 @@
+import * as d3 from 'd3';
+import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants';
+
+export const highlightIn = 1;
+export const highlightOut = 0.2;
+
+const getCurrent = (idx, collection) => d3.select(collection[idx]);
+const currentIsLive = (idx, collection) => getCurrent(idx, collection).classed(IS_HIGHLIGHTED);
+const getOtherLinks = () => d3.selectAll(`.${LINK_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
+const getNodesNotLive = () => d3.selectAll(`.${NODE_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
+
+const backgroundLinks = selection => selection.style('stroke-opacity', highlightOut);
+const backgroundNodes = selection => selection.attr('stroke', '#f2f2f2');
+const foregroundLinks = selection => selection.style('stroke-opacity', highlightIn);
+const foregroundNodes = selection => selection.attr('stroke', d => d.color);
+const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity);
+const renewNodes = selection => selection.attr('stroke', d => d.color);
+
+const getAllLinkAncestors = node => {
+ if (node.targetLinks) {
+ return node.targetLinks.flatMap(n => {
+ return [n.uid, ...getAllLinkAncestors(n.source)];
+ });
+ }
+
+ return [];
+};
+
+const getAllNodeAncestors = node => {
+ let allNodes = [];
+
+ if (node.targetLinks) {
+ allNodes = node.targetLinks.flatMap(n => {
+ return getAllNodeAncestors(n.source);
+ });
+ }
+
+ return [...allNodes, node.uid];
+};
+
+export const highlightLinks = (d, idx, collection) => {
+ const currentLink = getCurrent(idx, collection);
+ const currentSourceNode = d3.select(`#${d.source.uid}`);
+ const currentTargetNode = d3.select(`#${d.target.uid}`);
+
+ /* Higlight selected link, de-emphasize others */
+ backgroundLinks(getOtherLinks());
+ foregroundLinks(currentLink);
+
+ /* Do the same to related nodes */
+ backgroundNodes(getNodesNotLive());
+ foregroundNodes(currentSourceNode);
+ foregroundNodes(currentTargetNode);
+};
+
+const highlightPath = (parentLinks, parentNodes) => {
+ /* de-emphasize everything else */
+ backgroundLinks(getOtherLinks());
+ backgroundNodes(getNodesNotLive());
+
+ /* highlight correct links */
+ parentLinks.forEach(id => {
+ foregroundLinks(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
+ });
+
+ /* highlight correct nodes */
+ parentNodes.forEach(id => {
+ foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
+ });
+};
+
+const restorePath = (parentLinks, parentNodes, baseOpacity) => {
+ parentLinks.forEach(id => {
+ renewLinks(d3.select(`#${id}`), baseOpacity).classed(IS_HIGHLIGHTED, false);
+ });
+
+ parentNodes.forEach(id => {
+ d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false);
+ });
+
+ if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
+ renewLinks(getOtherLinks(), baseOpacity);
+ renewNodes(getNodesNotLive());
+ return;
+ }
+
+ backgroundLinks(getOtherLinks());
+ backgroundNodes(getNodesNotLive());
+};
+
+export const restoreLinks = (baseOpacity, d, idx, collection) => {
+ /* in this case, it has just been clicked */
+ if (currentIsLive(idx, collection)) {
+ return;
+ }
+
+ /*
+ if there exist live links, reset to highlight out / pale
+ otherwise, reset to base
+ */
+
+ if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
+ renewLinks(d3.selectAll(`.${LINK_SELECTOR}`), baseOpacity);
+ renewNodes(d3.selectAll(`.${NODE_SELECTOR}`));
+ return;
+ }
+
+ backgroundLinks(getOtherLinks());
+ backgroundNodes(getNodesNotLive());
+};
+
+export const toggleLinkHighlight = (baseOpacity, d, idx, collection) => {
+ if (currentIsLive(idx, collection)) {
+ restorePath([d.uid], [d.source.uid, d.target.uid], baseOpacity);
+ return;
+ }
+
+ highlightPath([d.uid], [d.source.uid, d.target.uid]);
+};
+
+export const togglePathHighlights = (baseOpacity, d, idx, collection) => {
+ const parentLinks = getAllLinkAncestors(d);
+ const parentNodes = getAllNodeAncestors(d);
+ const currentNode = getCurrent(idx, collection);
+
+ /* if this node is already live, make it unlive and reset its path */
+ if (currentIsLive(idx, collection)) {
+ currentNode.classed(IS_HIGHLIGHTED, false);
+ restorePath(parentLinks, parentNodes, baseOpacity);
+ return;
+ }
+
+ highlightPath(parentLinks, parentNodes);
+};
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
new file mode 100644
index 00000000000..8bdf043a106
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
+
+import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ GlLink,
+ },
+ model: {
+ prop: 'deleteAlertType',
+ event: 'change',
+ },
+ props: {
+ deleteAlertType: {
+ type: String,
+ default: null,
+ required: false,
+ validator(value) {
+ return !value || ALERT_MESSAGES[value] !== undefined;
+ },
+ },
+ garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
+ isAdmin: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ deleteAlertConfig() {
+ const config = {
+ title: '',
+ message: '',
+ type: 'success',
+ };
+ if (this.deleteAlertType) {
+ [config.type] = this.deleteAlertType.split('_');
+
+ config.message = ALERT_MESSAGES[this.deleteAlertType];
+
+ if (this.isAdmin && config.type === 'success') {
+ config.title = config.message;
+ config.message = ADMIN_GARBAGE_COLLECTION_TIP;
+ }
+ }
+ return config;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="deleteAlertType"
+ :variant="deleteAlertConfig.type"
+ :title="deleteAlertConfig.title"
+ @dismiss="$emit('change', null)"
+ >
+ <gl-sprintf :message="deleteAlertConfig.message">
+ <template #docLink="{content}">
+ <gl-link :href="garbageCollectionHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
new file mode 100644
index 00000000000..96f221bf71d
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
+
+export default {
+ components: {
+ GlModal,
+ GlSprintf,
+ },
+ props: {
+ itemsToBeDeleted: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ modalAction() {
+ return n__(
+ 'ContainerRegistry|Remove tag',
+ 'ContainerRegistry|Remove tags',
+ this.itemsToBeDeleted.length,
+ );
+ },
+ modalDescription() {
+ if (this.itemsToBeDeleted.length > 1) {
+ return {
+ message: REMOVE_TAGS_CONFIRMATION_TEXT,
+ item: this.itemsToBeDeleted.length,
+ };
+ }
+ const [first] = this.itemsToBeDeleted;
+
+ return {
+ message: REMOVE_TAG_CONFIRMATION_TEXT,
+ item: first?.path,
+ };
+ },
+ },
+ methods: {
+ show() {
+ this.$refs.deleteModal.show();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-tag-modal"
+ ok-variant="danger"
+ @ok="$emit('confirmDelete')"
+ @cancel="$emit('cancelDelete')"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <p v-if="modalDescription" data-testid="description">
+ <gl-sprintf :message="modalDescription.message">
+ <template #item
+ ><b>{{ modalDescription.item }}</b></template
+ >
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
new file mode 100644
index 00000000000..c254dd05aa4
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { DETAILS_PAGE_TITLE } from '../../constants/index';
+
+export default {
+ components: { GlSprintf },
+ props: {
+ imageName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ i18n: {
+ DETAILS_PAGE_TITLE,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-my-2 gl-align-items-center">
+ <h4>
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
+ <template #imageName>
+ {{ imageName }}
+ </template>
+ </gl-sprintf>
+ </h4>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index 0eeb6001772..a1fa995c17f 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -47,3 +47,14 @@ export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';
+export const ALERT_SUCCESS_TAG = 'success_tag';
+export const ALERT_DANGER_TAG = 'danger_tag';
+export const ALERT_SUCCESS_TAGS = 'success_tags';
+export const ALERT_DANGER_TAGS = 'danger_tags';
+
+export const ALERT_MESSAGES = {
+ [ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE,
+ [ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE,
+ [ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
+ [ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
+};
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 9b8ae16e4cd..31dbc1c8203 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -7,10 +7,6 @@ import {
GlIcon,
GlTooltipDirective,
GlPagination,
- GlModal,
- GlSprintf,
- GlAlert,
- GlLink,
GlEmptyState,
GlResizeObserverDirective,
GlSkeletonLoader,
@@ -21,6 +17,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import Tracking from '~/tracking';
+import DeleteAlert from '../components/details_page/delete_alert.vue';
+import DeleteModal from '../components/details_page/delete_modal.vue';
+import DetailsHeader from '../components/details_page/details_header.vue';
import { decodeAndParse } from '../utils';
import {
LIST_KEY_TAG,
@@ -33,34 +32,29 @@ import {
LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE,
LIST_LABEL_LAST_UPDATED,
- DELETE_TAG_SUCCESS_MESSAGE,
- DELETE_TAG_ERROR_MESSAGE,
- DELETE_TAGS_SUCCESS_MESSAGE,
- DELETE_TAGS_ERROR_MESSAGE,
- REMOVE_TAG_CONFIRMATION_TEXT,
- REMOVE_TAGS_CONFIRMATION_TEXT,
- DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
- ADMIN_GARBAGE_COLLECTION_TIP,
+ ALERT_SUCCESS_TAG,
+ ALERT_DANGER_TAG,
+ ALERT_SUCCESS_TAGS,
+ ALERT_DANGER_TAGS,
} from '../constants/index';
export default {
components: {
+ DeleteAlert,
+ DetailsHeader,
GlTable,
GlFormCheckbox,
GlDeprecatedButton,
GlIcon,
ClipboardButton,
GlPagination,
- GlModal,
+ DeleteModal,
GlSkeletonLoader,
- GlSprintf,
GlEmptyState,
- GlAlert,
- GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -73,18 +67,11 @@ export default {
height: 40,
},
i18n: {
- DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
},
- alertMessages: {
- success_tag: DELETE_TAG_SUCCESS_MESSAGE,
- danger_tag: DELETE_TAG_ERROR_MESSAGE,
- success_tags: DELETE_TAGS_SUCCESS_MESSAGE,
- danger_tags: DELETE_TAGS_ERROR_MESSAGE,
- },
data() {
return {
selectedItems: [],
@@ -92,7 +79,7 @@ export default {
selectAllChecked: false,
modalDescription: null,
isDesktop: true,
- deleteAlertType: false,
+ deleteAlertType: null,
};
},
computed: {
@@ -119,21 +106,12 @@ export default {
{ key: LIST_KEY_ACTIONS, label: '' },
].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
},
- isMultiDelete() {
- return this.itemsToBeDeleted.length > 1;
- },
tracking() {
return {
- label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
+ label:
+ this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
};
},
- modalAction() {
- return n__(
- 'ContainerRegistry|Remove tag',
- 'ContainerRegistry|Remove tags',
- this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
- );
- },
currentPage: {
get() {
return this.tagsPagination.page;
@@ -142,47 +120,12 @@ export default {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
},
},
- deleteAlertConfig() {
- const config = {
- title: '',
- message: '',
- type: 'success',
- };
- if (this.deleteAlertType) {
- [config.type] = this.deleteAlertType.split('_');
-
- const defaultMessage = this.$options.alertMessages[this.deleteAlertType];
-
- if (this.config.isAdmin && config.type === 'success') {
- config.title = defaultMessage;
- config.message = ADMIN_GARBAGE_COLLECTION_TIP;
- } else {
- config.message = defaultMessage;
- }
- }
- return config;
- },
},
mounted() {
this.requestTagsList({ params: this.$route.params.id });
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
- setModalDescription(itemIndex = -1) {
- if (itemIndex === -1) {
- this.modalDescription = {
- message: REMOVE_TAGS_CONFIRMATION_TEXT,
- item: this.itemsToBeDeleted.length,
- };
- } else {
- const { path } = this.tags[itemIndex];
-
- this.modalDescription = {
- message: REMOVE_TAG_CONFIRMATION_TEXT,
- item: path,
- };
- }
- },
formatSize(size) {
return numberToHumanSize(size);
},
@@ -197,53 +140,49 @@ export default {
}
},
selectAll() {
- this.selectedItems = this.tags.map((x, index) => index);
+ this.selectedItems = this.tags.map(x => x.name);
this.selectAllChecked = true;
},
deselectAll() {
this.selectedItems = [];
this.selectAllChecked = false;
},
- updateSelectedItems(index) {
- const delIndex = this.selectedItems.findIndex(x => x === index);
+ updateSelectedItems(name) {
+ const delIndex = this.selectedItems.findIndex(x => x === name);
if (delIndex > -1) {
this.selectedItems.splice(delIndex, 1);
this.selectAllChecked = false;
} else {
- this.selectedItems.push(index);
+ this.selectedItems.push(name);
if (this.selectedItems.length === this.tags.length) {
this.selectAllChecked = true;
}
}
},
- deleteSingleItem(index) {
- this.setModalDescription(index);
- this.itemsToBeDeleted = [index];
+ deleteSingleItem(name) {
+ this.itemsToBeDeleted = [{ ...this.tags.find(t => t.name === name) }];
this.track('click_button');
this.$refs.deleteModal.show();
},
deleteMultipleItems() {
- this.itemsToBeDeleted = [...this.selectedItems];
- if (this.selectedItems.length === 1) {
- this.setModalDescription(this.itemsToBeDeleted[0]);
- } else if (this.selectedItems.length > 1) {
- this.setModalDescription();
- }
+ this.itemsToBeDeleted = this.selectedItems.map(name => ({
+ ...this.tags.find(t => t.name === name),
+ }));
this.track('click_button');
this.$refs.deleteModal.show();
},
- handleSingleDelete(index) {
- const itemToDelete = this.tags[index];
+ handleSingleDelete() {
+ const [itemToDelete] = this.itemsToBeDeleted;
this.itemsToBeDeleted = [];
- this.selectedItems = this.selectedItems.filter(i => i !== index);
+ this.selectedItems = this.selectedItems.filter(name => name !== itemToDelete.name);
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
.then(() => {
- this.deleteAlertType = 'success_tag';
+ this.deleteAlertType = ALERT_SUCCESS_TAG;
})
.catch(() => {
- this.deleteAlertType = 'danger_tag';
+ this.deleteAlertType = ALERT_DANGER_TAG;
});
},
handleMultipleDelete() {
@@ -252,22 +191,22 @@ export default {
this.selectedItems = [];
return this.requestDeleteTags({
- ids: itemsToBeDeleted.map(x => this.tags[x].name),
+ ids: itemsToBeDeleted.map(x => x.name),
params: this.$route.params.id,
})
.then(() => {
- this.deleteAlertType = 'success_tags';
+ this.deleteAlertType = ALERT_SUCCESS_TAGS;
})
.catch(() => {
- this.deleteAlertType = 'danger_tags';
+ this.deleteAlertType = ALERT_DANGER_TAGS;
});
},
onDeletionConfirmed() {
this.track('confirm_delete');
- if (this.isMultiDelete) {
+ if (this.itemsToBeDeleted.length > 1) {
this.handleMultipleDelete();
} else {
- this.handleSingleDelete(this.itemsToBeDeleted[0]);
+ this.handleSingleDelete();
}
},
handleResize() {
@@ -279,30 +218,14 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
- <gl-alert
- v-if="deleteAlertType"
- :variant="deleteAlertConfig.type"
- :title="deleteAlertConfig.title"
+ <delete-alert
+ v-model="deleteAlertType"
+ :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :is-admin="config.isAdmin"
class="my-2"
- @dismiss="deleteAlertType = null"
- >
- <gl-sprintf :message="deleteAlertConfig.message">
- <template #docLink="{content}">
- <gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <div class="d-flex my-3 align-items-center">
- <h4>
- <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
- <template #imageName>
- {{ imageName }}
- </template>
- </gl-sprintf>
- </h4>
- </div>
+ />
+
+ <details-header :image-name="imageName" />
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
<template v-if="isDesktop" #head(checkbox)>
@@ -327,12 +250,12 @@ export default {
</gl-deprecated-button>
</template>
- <template #cell(checkbox)="{index}">
+ <template #cell(checkbox)="{item}">
<gl-form-checkbox
ref="rowCheckbox"
class="js-row-checkbox"
- :checked="selectedItems.includes(index)"
- @change="updateSelectedItems(index)"
+ :checked="selectedItems.includes(item.name)"
+ @change="updateSelectedItems(item.name)"
/>
</template>
<template #cell(name)="{item, field}">
@@ -373,7 +296,7 @@ export default {
{{ timeFormatted(value) }}
</span>
</template>
- <template #cell(actions)="{index, item}">
+ <template #cell(actions)="{item}">
<gl-deprecated-button
ref="singleDeleteButton"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
@@ -381,7 +304,7 @@ export default {
:disabled="!item.destroy_path"
variant="danger"
class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
- @click="deleteSingleItem(index)"
+ @click="deleteSingleItem(item.name)"
>
<gl-icon name="remove" />
</gl-deprecated-button>
@@ -425,22 +348,11 @@ export default {
class="w-100"
/>
- <gl-modal
+ <delete-modal
ref="deleteModal"
- modal-id="delete-tag-modal"
- ok-variant="danger"
- @ok="onDeletionConfirmed"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirmDelete="onDeletionConfirmed"
@cancel="track('cancel_delete')"
- >
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
- <p v-if="modalDescription">
- <gl-sprintf :message="modalDescription.message">
- <template #item>
- <b>{{ modalDescription.item }}</b>
- </template>
- </gl-sprintf>
- </p>
- </gl-modal>
+ />
</div>
</template>
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 11064f18418..e4bee01f61f 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -137,7 +137,7 @@
.badge.badge-pill:not(.fly-out-badge),
.sidebar-context-title,
.nav-item-name {
- display: none;
+ @include gl-sr-only;
}
.sidebar-top-level-items > li > a {
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0b1b3f2bcba..98fa8202e25 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -16,19 +16,6 @@ module IssuableActions
end
end
- def permitted_keys
- [
- :issuable_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- ]
- end
-
def show
respond_to do |format|
format.html do
@@ -221,10 +208,20 @@ module IssuableActions
end
def bulk_update_params
- permitted_keys_array = permitted_keys.dup
- permitted_keys_array << { assignee_ids: [] }
+ params.require(:update).permit(bulk_update_permitted_keys)
+ end
- params.require(:update).permit(permitted_keys_array)
+ def bulk_update_permitted_keys
+ [
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ assignee_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ ]
end
def resource_name
diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
index dfda5fca310..e3cef427fd6 100644
--- a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
+++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
@@ -7,7 +7,7 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati
REPORT_WINDOW = 90.days
before_action :validate_feature_flag!
- before_action :authorize_download_code! # Share the same authorization rules as the graphs controller
+ before_action :authorize_read_build_report_results!
before_action :validate_param_type!
def index
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 34246f27241..856601dfc01 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -6,7 +6,7 @@ class Projects::GraphsController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :assign_ref_vars
- before_action :authorize_download_code!
+ before_action :authorize_read_repository_graphs!
def show
respond_to do |format|
@@ -54,7 +54,8 @@ class Projects::GraphsController < Projects::ApplicationController
end
def get_daily_coverage_options
- return unless Feature.enabled?(:ci_download_daily_code_coverage, default_enabled: true)
+ return unless Feature.enabled?(:ci_download_daily_code_coverage, @project, default_enabled: true)
+ return unless can?(current_user, :read_build_report_results, project)
date_today = Date.current
report_window = Projects::Ci::DailyBuildGroupReportResultsController::REPORT_WINDOW
diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb
index 3c3c24c1479..3ce38f39b0b 100644
--- a/app/finders/ci/daily_build_group_report_results_finder.rb
+++ b/app/finders/ci/daily_build_group_report_results_finder.rb
@@ -14,7 +14,7 @@ module Ci
end
def execute
- return none unless can?(current_user, :download_code, project)
+ return none unless can?(current_user, :read_build_report_results, project)
Ci::DailyBuildGroupReportResult.recent_results(
{
diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb
index 9560989a421..c1e45808593 100644
--- a/app/graphql/mutations/merge_requests/set_labels.rb
+++ b/app/graphql/mutations/merge_requests/set_labels.rb
@@ -24,8 +24,9 @@ module Mutations
project = merge_request.project
label_ids = label_ids
+ .map { |gid| GlobalID.parse(gid) }
.select(&method(:label_descendant?))
- .map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers
+ .map(&:model_id) # MergeRequests::UpdateService expects integers
attribute_name = case operation_mode
when Types::MutationOperationModeEnum.enum[:append]
@@ -46,7 +47,7 @@ module Mutations
end
def label_descendant?(gid)
- GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label)
+ gid&.model_class&.ancestors&.include?(Label)
end
end
end
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 6be98fe3d44..55c9084caf2 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows)
+ Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/event.rb b/app/models/event.rb
index 40f412af7c4..03a43a6e93c 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -76,7 +76,7 @@ class Event < ApplicationRecord
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push_action?
- after_create :track_user_interacted_projects
+ after_create ->(event) { UserInteractedProject.track(event) }
# Scopes
scope :recent, -> { reorder(id: :desc) }
@@ -429,13 +429,6 @@ class Event < ApplicationRecord
.update_all(last_repository_updated_at: created_at)
end
- def track_user_interacted_projects
- # Note the call to .available? is due to earlier migrations
- # that would otherwise conflict with the call to .track
- # (because the table does not exist yet).
- UserInteractedProject.track(self) if UserInteractedProject.available?
- end
-
def design_action_names
{
created: _('uploaded'),
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index b6882701e23..21cf6bfa414 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -25,8 +25,6 @@ class InternalId < ApplicationRecord
validates :usage, presence: true
- REQUIRED_SCHEMA_VERSION = 20180305095250
-
# Increments #last_value and saves the record
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
@@ -63,24 +61,16 @@ class InternalId < ApplicationRecord
class << self
def track_greatest(subject, scope, usage, new_value, init)
- return new_value unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.track_greatest(init, new_value)
end
def generate_next(subject, scope, usage, init)
- # Shortcut if `internal_ids` table is not available (yet)
- # This can be the case in other (unrelated) migration specs
- return (init.call(subject) || 0) + 1 unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.generate(init)
end
def reset(subject, scope, usage, value)
- return false unless available?
-
InternalIdGenerator.new(subject, scope, usage)
.reset(value)
end
@@ -95,20 +85,6 @@ class InternalId < ApplicationRecord
where(filter).delete_all
end
-
- def available?
- return true unless Rails.env.test?
-
- Gitlab::SafeRequestStore.fetch(:internal_ids_available_flag) do
- ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION
- end
- end
-
- # Flushes cached information about schema
- def reset_column_information
- Gitlab::SafeRequestStore[:internal_ids_available_flag] = nil
- super
- end
end
class InternalIdGenerator
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index eecb10e6dbc..de97fc33f8d 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -20,7 +20,7 @@ class MergeRequestContextCommit < ApplicationRecord
# create MergeRequestContextCommit by given commit sha and it's diff file record
def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', *args)
+ Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 9dce7c53ab6..b89d1983ce3 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -12,6 +12,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args)
+ Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index f793bd3d76f..66b27aeac91 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -437,7 +437,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -495,7 +495,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 2819ea7ce1e..9f6933d0879 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -27,6 +27,6 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.bulk_insert(self.table_name, rows)
+ Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2d564c8c81d..dcd9f2aac2e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -96,8 +96,7 @@ class Project < ApplicationRecord
after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
- unless: :ci_cd_settings,
- if: proc { ProjectCiCdSetting.available? }
+ unless: :ci_cd_settings
after_create :create_container_expiration_policy,
unless: :container_expiration_policy
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index c295837002a..e5fc481b035 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -3,9 +3,6 @@
class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
- # The version of the schema that first introduced this model/table.
- MINIMUM_SCHEMA_VERSION = 20180403035759
-
DEFAULT_GIT_DEPTH = 50
before_create :set_default_git_depth
@@ -20,16 +17,6 @@ class ProjectCiCdSetting < ApplicationRecord
default_value_for :forward_deployment_enabled, true
- def self.available?
- @available ||=
- ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
- end
-
- def self.reset_column_information
- @available = nil
- super
- end
-
def forward_deployment_enabled?
super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true)
end
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index f6f72f4b77a..1c615777018 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -9,9 +9,6 @@ class UserInteractedProject < ApplicationRecord
CACHE_EXPIRY_TIME = 1.day
- # Schema version required for this model
- REQUIRED_SCHEMA_VERSION = 20180223120443
-
class << self
def track(event)
# For events without a project, we simply don't care.
@@ -38,17 +35,6 @@ class UserInteractedProject < ApplicationRecord
end
end
- # Check if we can safely call .track (table exists)
- def available?
- @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
- end
-
- # Flushes cached information about schema
- def reset_column_information
- @available_flag = nil
- super
- end
-
private
def cached_exists?(project_id:, user_id:, &block)
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 156a466b3f8..f87c72007ec 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -568,6 +568,14 @@ class ProjectPolicy < BasePolicy
rule { build_service_proxy_enabled }.enable :build_service_proxy_enabled
+ rule { can?(:download_code) }.policy do
+ enable :read_repository_graphs
+ end
+
+ rule { can?(:read_build) & can?(:read_pipeline) }.policy do
+ enable :read_build_report_results
+ end
+
private
def team_member?
diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb
index 97f9918fdb7..c756e376901 100644
--- a/app/services/ci/extract_sections_from_build_trace_service.rb
+++ b/app/services/ci/extract_sections_from_build_trace_service.rb
@@ -5,7 +5,7 @@ module Ci
def execute(build)
return false unless build.trace_sections.empty?
- Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build))
+ Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) # rubocop:disable Gitlab/BulkInsert
true
end
diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb
index 34d44ab881e..50c4e26b0d0 100644
--- a/app/services/clusters/applications/prometheus_config_service.rb
+++ b/app/services/clusters/applications/prometheus_config_service.rb
@@ -132,19 +132,21 @@ module Clusters
end
def alerts(environment)
- variables = Gitlab::Prometheus::QueryVariables.call(environment)
alerts = Projects::Prometheus::AlertsFinder
.new(environment: environment)
.execute
alerts.map do |alert|
- substitute_query_variables(alert.to_param, variables)
+ hash = alert.to_param
+ hash['expr'] = substitute_query_variables(hash['expr'], environment)
+ hash
end
end
- def substitute_query_variables(hash, variables)
- hash['expr'] %= variables
- hash
+ def substitute_query_variables(query, environment)
+ result = ::Prometheus::ProxyVariableSubstitutionService.new(environment, query: query).execute
+
+ result[:params][:query]
end
def environments
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 1518b697f86..2902385da4a 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -17,9 +17,8 @@ module Issuable
ids = params.delete(:issuable_ids).split(",")
items = find_issuables(parent, model_class, ids)
- permitted_attrs(type).each do |key|
- params.delete(key) unless params[key].present?
- end
+ params.slice!(*permitted_attrs(type))
+ params.delete_if { |k, v| v.blank? }
if params[:assignee_ids] == [IssuableFinder::Params::NONE.to_s]
params[:assignee_ids] = []
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index a78e191c85f..b185ab592ff 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -105,7 +105,7 @@ module Issuable
yield(event)
end.compact
- Gitlab::Database.bulk_insert(table_name, events)
+ Gitlab::Database.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3fa5b60369c..38b10996f44 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -129,15 +129,11 @@ class IssuableBaseService < BaseService
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
- new_label_ids = existing_label_ids || label_ids || []
+ new_label_ids = label_ids || existing_label_ids || []
new_label_ids |= extra_label_ids
- if add_label_ids.blank? && remove_label_ids.blank?
- new_label_ids = label_ids if label_ids
- else
- new_label_ids |= add_label_ids if add_label_ids
- new_label_ids -= remove_label_ids if remove_label_ids
- end
+ new_label_ids |= add_label_ids if add_label_ids
+ new_label_ids -= remove_label_ids if remove_label_ids
new_label_ids.uniq
end
diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb
index 942cd8162e4..c57773c3302 100644
--- a/app/services/projects/detect_repository_languages_service.rb
+++ b/app/services/projects/detect_repository_languages_service.rb
@@ -21,7 +21,7 @@ module Projects
.update_all(share: update[:share])
end
- Gitlab::Database.bulk_insert(
+ Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
RepositoryLanguage.table_name,
detection.insertions(matching_programming_languages)
)
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 39cd553261f..e86106f0a09 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -38,7 +38,7 @@ module Projects
rows = existent_lfs_objects
.not_linked_to_project(project)
.map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } }
- Gitlab::Database.bulk_insert(:lfs_objects_projects, rows)
+ Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
iterations += 1
linked_existing_objects += existent_lfs_objects.map(&:oid)
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index e0d019f54be..dc23f727079 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -22,7 +22,7 @@ module ResourceEvents
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
- Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels)
+ Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
resource.expire_note_etag_cache
end
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index 1d3338c1b45..93d2bd11426 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -25,7 +25,7 @@ module Suggestions
end
rows.in_groups_of(100, false) do |rows|
- Gitlab::Database.bulk_insert('suggestions', rows)
+ Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
end
end
end
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 42528f40123..b0593b3bfa2 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -14,10 +14,10 @@
.form-group
= f.label :outbound_local_requests_whitelist_raw, class: 'label-bold' do
- = _('Whitelist to allow requests to the local network from hooks and services')
+ = _('Local IP addresses and domain names that hooks and services may access.')
= f.text_area :outbound_local_requests_whitelist_raw, placeholder: "example.com, 192.168.1.1", class: 'form-control', rows: 8
%span.form-text.text-muted
- = _('Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com.')
+ = _('Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The allowlist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com.')
.form-group
.form-check
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
index 507dd91d571..7119b22daef 100644
--- a/app/views/projects/issues/import_csv/_button.html.haml
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -1,4 +1,5 @@
- type = local_assigns.fetch(:type, :icon)
+- can_edit = can?(current_user, :admin_project, @project)
.dropdown.btn-group
%button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon),
@@ -9,6 +10,7 @@
= _('Import issues')
%ul.dropdown-menu
%li
- %button.btn{ data: { toggle: 'modal', target: '.issues-import-modal' } }
+ %button{ data: { toggle: 'modal', target: '.issues-import-modal' } }
= _('Import CSV')
- %li= link_to _('Import from Jira'), project_import_jira_path(@project)
+ - if can_edit
+ %li= link_to _('Import from Jira'), project_import_jira_path(@project)
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index 78de5cf1307..89f5fe8d462 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -48,7 +48,7 @@ module Gitlab
label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i)
- Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs)
+ Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert
end
def assign_issue(project_id, issue_id, assignee_ids)
@@ -56,7 +56,7 @@ module Gitlab
assignee_attrs = assignee_ids.map { |user_id| { issue_id: issue_id, user_id: user_id } }
- Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs)
+ Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert
end
def build_label_attrs(issue_id, label_id)
diff --git a/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml b/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml
new file mode 100644
index 00000000000..da712f0aa59
--- /dev/null
+++ b/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml
@@ -0,0 +1,5 @@
+---
+title: Hide "Import from Jira" option from non-entitled users
+merge_request: 32685
+author:
+type: fixed
diff --git a/changelogs/unreleased/220144-substitute-variables-in-alerts.yml b/changelogs/unreleased/220144-substitute-variables-in-alerts.yml
new file mode 100644
index 00000000000..b270feee3eb
--- /dev/null
+++ b/changelogs/unreleased/220144-substitute-variables-in-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug with variable substitution in alerts
+merge_request: 33772
+author:
+type: fixed
diff --git a/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml b/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml
new file mode 100644
index 00000000000..b222b69f295
--- /dev/null
+++ b/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml
@@ -0,0 +1,5 @@
+---
+title: Update Auto deploy image to v0.16.1, introducing support for AUTO_DEVOPS_DEPLOY_DEBUG
+merge_request: 33799
+author:
+type: changed
diff --git a/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml b/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml
new file mode 100644
index 00000000000..0d7f75d24b8
--- /dev/null
+++ b/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Add link text to collapsed left sidebar links for screen readers
+merge_request: 33866
+author:
+type: fixed
diff --git a/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml b/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml
new file mode 100644
index 00000000000..80d2ec6d7ba
--- /dev/null
+++ b/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml
@@ -0,0 +1,5 @@
+---
+title: Update local IP address and domain name allow list input label
+merge_request: 33812
+author:
+type: changed
diff --git a/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml b/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml
new file mode 100644
index 00000000000..22e72c5bb8f
--- /dev/null
+++ b/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml
@@ -0,0 +1,5 @@
+---
+title: Add whether instance has Auto DevOps enabled to usage ping
+merge_request: 33811
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-avoid-extra-route-reload.yml b/changelogs/unreleased/sh-avoid-extra-route-reload.yml
new file mode 100644
index 00000000000..7c363ac92d8
--- /dev/null
+++ b/changelogs/unreleased/sh-avoid-extra-route-reload.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up boot time in production
+merge_request: 33929
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-workhorse-direct-access-upload.yml b/changelogs/unreleased/sh-workhorse-direct-access-upload.yml
new file mode 100644
index 00000000000..41aee482891
--- /dev/null
+++ b/changelogs/unreleased/sh-workhorse-direct-access-upload.yml
@@ -0,0 +1,5 @@
+---
+title: Support Workhorse directly uploading files to S3
+merge_request: 29389
+author:
+type: added
diff --git a/changelogs/unreleased/templates-current-folder-fix.yml b/changelogs/unreleased/templates-current-folder-fix.yml
new file mode 100644
index 00000000000..9b4999c9d54
--- /dev/null
+++ b/changelogs/unreleased/templates-current-folder-fix.yml
@@ -0,0 +1,5 @@
+---
+title: "Web IDE: Create template files in the folder from which new file request was made"
+merge_request: 33585
+author: Ashesh Vidyut
+type: fixed
diff --git a/config/application.rb b/config/application.rb
index df1ff971203..d0c211bf608 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -301,7 +301,10 @@ module Gitlab
end
config.after_initialize do
- Rails.application.reload_routes!
+ # Devise (see initializers/8_devise.rb) already reloads routes if
+ # eager loading is enabled, so don't do this twice since it's
+ # expensive.
+ Rails.application.reload_routes! unless config.eager_load
project_url_helpers = Module.new do
extend ActiveSupport::Concern
diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb
index 6b4dc91ed86..2be6e535fee 100644
--- a/config/initializers/8_devise.rb
+++ b/config/initializers/8_devise.rb
@@ -6,6 +6,11 @@ Devise.setup do |config|
manager.default_strategies(scope: :user).unshift :two_factor_backupable
end
+ # This is the default. This makes it explicit that Devise loads routes
+ # before eager loading. Disabling this seems to cause an error loading
+ # grape-entity `expose` for some reason.
+ config.reload_routes = true
+
# ==> Mailer Configuration
# Configure the class responsible to send e-mails.
config.mailer = "DeviseMailer"
diff --git a/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb
index e2ec7b62d31..b41c55ce622 100644
--- a/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb
+++ b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb
@@ -20,7 +20,7 @@ class MigrateSamlIdentitiesToScimIdentities < ActiveRecord::Migration[6.0]
record.attributes.extract!("extern_uid", "user_id", "group_id", "active", "created_at", "updated_at")
end
- Gitlab::Database.bulk_insert(:scim_identities, data_to_insert, on_conflict: :do_nothing)
+ Gitlab::Database.bulk_insert(:scim_identities, data_to_insert, on_conflict: :do_nothing) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 3b98ac7316d..3c2812d8b52 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -322,28 +322,6 @@ application server, or a Gitaly node.
}
```
-1. Enable automatic failover by editing `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- praefect['failover_enabled'] = true
- praefect['failover_election_strategy'] = 'sql'
- ```
-
- When automatic failover is enabled, Praefect checks the health of internal
- Gitaly nodes. If the primary has a certain amount of health checks fail, it
- will promote one of the secondaries to be primary, and demote the primary to
- be a secondary.
-
- NOTE: **Note:** Database leader election will be [enabled by default in the
- future](https://gitlab.com/gitlab-org/gitaly/-/issues/2682).
-
- Caution, **automatic failover** favors availability over consistency and will
- cause data loss if changes have not been replicated to the newly elected
- primary. In the next release, leader election will [prefer to promote up to
- date replicas](https://gitlab.com/gitlab-org/gitaly/-/issues/2642), and it
- will be an option to favor consistency by marking [out-of-date repositories
- read-only](https://gitlab.com/gitlab-org/gitaly/-/issues/2630).
-
1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
@@ -738,7 +716,7 @@ Praefect regularly checks the health of each backend Gitaly node. This
information can be used to automatically failover to a new primary node if the
current primary node is found to be unhealthy.
-- **PostgreSQL (recommended):** Enabled by setting
+- **PostgreSQL (recommended):** Enabled by default, and equivalent to:
`praefect['failover_election_strategy'] = sql`. This configuration
option will allow multiple Praefect nodes to coordinate via the
PostgreSQL database to elect a primary Gitaly node. This configuration
@@ -749,18 +727,13 @@ current primary node is found to be unhealthy.
reconfigured in `/etc/gitlab/gitlab.rb` on the Praefect node. Modify the
`praefect['virtual_storages']` field by moving the `primary = true` to promote
a different Gitaly node to primary. In the steps above, `gitaly-1` was set to
- the primary.
-- **Memory:** Enabled by setting `praefect['failover_enabled'] = true` in
- `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health
+ the primary. Requires `praefect['failover_enabled'] = false` in the configuration.
+- **Memory:** Enabled by setting `praefect['failover_election_strategy'] = 'local'`
+ in `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health
checks fail for the current primary backend Gitaly node, and new primary will
be elected. **Do not use with multiple Praefect nodes!** Using with multiple
Praefect nodes is likely to result in a split brain.
-NOTE: **Note:**: Praefect does not yet account for replication lag on
-the secondaries during the election process, so data loss can occur
-during a failover. Follow issue
-[#2642](https://gitlab.com/gitlab-org/gitaly/-/issues/2642) for updates.
-
It is likely that we will implement support for Consul, and a cloud native
strategy in the future.
diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md
index 39819ccd79b..1dea2de73f6 100644
--- a/doc/administration/object_storage.md
+++ b/doc/administration/object_storage.md
@@ -141,10 +141,88 @@ Using the default GitLab settings, some object storage back-ends such as
and [Alibaba](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/1564)
might generate `ETag mismatch` errors.
+If you are seeing this ETag mismatch error with Amazon Web Services S3,
+it's likely this is due to [encryption settings on your bucket](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html).
+See the section on [using Amazon instance profiles](#using-amazon-instance-profiles) on how to fix this issue.
+
When using GitLab direct upload, the
[workaround for MinIO](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/1564#note_244497658)
is to use the `--compat` parameter on the server.
-We are working on a fix to GitLab component Workhorse, and also
-a workaround, in the mean time, to
-[allow ETag verification to be disabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18175).
+We are working on a fix to the [GitLab Workhorse
+component](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222).
+
+### Using Amazon instance profiles
+
+Instead of supplying AWS access and secret keys in object storage
+configuration, GitLab can be configured to use IAM roles to set up an
+[Amazon instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html).
+When this is used, GitLab will fetch temporary credentials each time an
+S3 bucket is accessed, so no hard-coded values are needed in the
+configuration.
+
+#### Encrypted S3 buckets
+
+> Introduced in [GitLab 13.1](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/466) only for instance profiles.
+
+When configured to use an instance profile, GitLab Workhorse
+will properly upload files to S3 buckets that have [SSE-S3 or SSE-KMS
+encryption enabled by default](https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html).
+Note that customer master keys (CMKs) and SSE-C encryption are not yet
+supported since this requires supplying keys to the GitLab
+configuration.
+
+Without instance profiles enabled (or prior to GitLab 13.1), GitLab
+Workhorse will upload files to S3 using pre-signed URLs that do not have
+a `Content-MD5` HTTP header computed for them. To ensure data is not
+corrupted, Workhorse checks that the MD5 hash of the data sent equals
+the ETag header returned from the S3 server. When encryption is enabled,
+this is not the case, which causes Workhorse to report an `ETag
+mismatch` error during an upload.
+
+With instance profiles enabled, GitLab Workhorse uses an AWS S3 client
+that properly computes and sends the `Content-MD5` header to the server,
+which eliminates the need for comparing ETag headers. If the data is
+corrupted in transit, the S3 server will reject the file.
+
+#### IAM Permissions
+
+To set up an instance profile, create an Amazon Identity Access and
+Management (IAM) role with the necessary permissions. The following
+example is a role for an S3 bucket named `test-bucket`:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "VisualEditor0",
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:AbortMultipartUpload",
+ "s3:DeleteObject"
+ ],
+ "Resource": "arn:aws:s3:::test-bucket/*"
+ }
+ ]
+}
+```
+
+Associate this role with your GitLab instance, and then configure GitLab
+to use it via the `use_iam_profile` configuration option. For example,
+when configuring uploads to use object storage, see the `AWS IAM profiles`
+section in [S3 compatible connection settings](uploads.md#s3-compatible-connection-settings).
+
+#### Disabling the feature
+
+The Workhorse S3 client is only enabled when the `use_iam_profile`
+configuration flag is `true`.
+
+To disable this feature, ask a GitLab administrator with [Rails console access](feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
+following command:
+
+```ruby
+Feature.disable(:use_workhorse_s3_client)
+```
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 7e330682f8c..33af356b37d 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -380,39 +380,6 @@ user = User.find_by_username ''
user.skip_reconfirmation!
```
-### Get an admin token
-
-```ruby
-# Get the first admin's first access token (no longer works on 11.9+. see: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22743)
-User.where(admin:true).first.personal_access_tokens.first.token
-
-# Get the first admin's private token (no longer works on 10.2+)
-User.where(admin:true).private_token
-```
-
-### Create personal access token
-
-```ruby
-personal_access_token = User.find(123).personal_access_tokens.create(
- name: 'apitoken',
- impersonation: false,
- scopes: [:api]
-)
-
-puts personal_access_token.token
-```
-
-You might also want to manually set the token string:
-
-```ruby
-User.find(123).personal_access_tokens.create(
- name: 'apitoken',
- token_digest: Gitlab::CryptoHelper.sha256('some-token-string-here'),
- impersonation: false,
- scopes: [:api]
-)
-```
-
### Active users & Historical users
```ruby
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index e963bdd6848..bdd372e90ed 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -263,7 +263,7 @@ n_("%{project_name}", "%d projects selected", count) % { project_name: 'GitLab'
A namespace is a way to group translations that belong together. They provide context to our translators by adding a prefix followed by the bar symbol (`|`). For example:
```ruby
-_('Namespace|Translated string')
+'Namespace|Translated string'
```
A namespace provide the following benefits:
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index c7e0aa13adb..441ef545141 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -310,6 +310,7 @@ applications.
| `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | From GitLab 11.11, used to set the name of the Helm repository. Defaults to `gitlab`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | From GitLab 11.11, used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | From GitLab 11.11, used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
+| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm will output debug logs. |
| `AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE` | From GitLab 12.5, used in combination with [ModSecurity feature flag](../../user/clusters/applications.md#web-application-firewall-modsecurity) to toggle [ModSecurity's `SecRuleEngine`](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRuleEngine) behavior. Defaults to `DetectionOnly`. |
| `BUILDPACK_URL` | Buildpack's full URL. Can point to either [a Git repository URL or a tarball URL](#custom-buildpacks). |
| `CANARY_ENABLED` | From GitLab 11.0, used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments-premium). |
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 87c1fe4007a..377b6cd393b 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -56,6 +56,58 @@ the following table.
| `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Allows read-only access (pull) to the repository through `git clone`. |
| `write_repository` | [GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26021) | Allows read-write access (pull, push) to the repository through `git clone`. Required for accessing Git repositories over HTTP when 2FA is enabled. |
+## Programmatically creating a personal access token
+
+You can programmatically create a predetermined personal access token for use in
+automation or tests. You will need sufficient access to run a
+[Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session)
+for your GitLab instance.
+
+To create a token belonging to a user with username `automation-bot`, run the
+following in the Rails console (`sudo gitlab-rails console`):
+
+```ruby
+user = User.find_by_username('automation-bot')
+token = user.personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token')
+token.set_token('token-string-here123')
+token.save!
+```
+
+This can be shortened into a single-line shell command using the
+[GitLab Rails Runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
+
+```shell
+sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!"
+```
+
+NOTE: **Note:**
+The token string must be 20 characters in length, or it will not be
+recognized as a personal access token.
+
+The list of valid scopes and what they do can be found
+[in the source code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/auth.rb).
+
+## Programmatically revoking a personal access token
+
+You can programmatically revoke a personal access token. You will need
+sufficient access to run a [Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session)
+for your GitLab instance.
+
+To revoke a known token `token-string-here123`, run the following in the Rails
+console (`sudo gitlab-rails console`):
+
+```ruby
+token = PersonalAccessToken.find_by_token('token-string-here123')
+token.revoke!
+```
+
+This can be shorted into a single-line shell command using the
+[GitLab Rails Runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
+
+```shell
+sudo gitlab-rails runner "PersonalAccessToken.find_by_token('token-string-here123').revoke!"
+```
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md
index fff3cf546b3..0b8807bb9b3 100644
--- a/doc/user/project/import/jira.md
+++ b/doc/user/project/import/jira.md
@@ -49,6 +49,7 @@ Importing large projects may take several minutes depending on the size of the i
1. On the **{issues}** **Issues** page, click the **Import Issues** (**{import}**) button.
1. Select **Import from Jira**.
+ This option is only visible if you have the [correct permissions](#permissions).
![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png)
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index fbecaf9ef72..ffb1f6a1407 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -173,12 +173,12 @@ As a result, a new Service Desk issue is created from this email in the `mygroup
#### Enable custom email address
-This feature comes with the `service_desk_email` feature flag disabled by default.
+This feature comes with the `service_desk_custom_address` feature flag disabled by default.
To turn on the feature, ask a GitLab administrator with Rails console access to run the following
command:
```ruby
-Feature.enable(:service_desk_email)
+Feature.enable(:service_desk_custom_address)
```
The configuration options are the same as for configuring
diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb
index 263546bd132..bc113a1e33d 100644
--- a/lib/gitlab/background_migration/backfill_project_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_project_repositories.rb
@@ -189,7 +189,7 @@ module Gitlab
end
def perform(start_id, stop_id)
- Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id))
+ Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id)) # rubocop:disable Gitlab/BulkInsert
end
private
diff --git a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
index 899f381e911..d2a9939b9ee 100644
--- a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
+++ b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
@@ -34,7 +34,7 @@ module Gitlab
end
end
- Gitlab::Database.bulk_insert(TEMP_TABLE, fingerprints)
+ Gitlab::Database.bulk_insert(TEMP_TABLE, fingerprints) # rubocop:disable Gitlab/BulkInsert
execute("ANALYZE #{TEMP_TABLE}")
diff --git a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
index 956f9daa493..2bce5037d03 100644
--- a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
+++ b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
@@ -65,7 +65,7 @@ module Gitlab
next if service_ids.empty?
migrated_ids += service_ids
- Gitlab::Database.bulk_insert(table, data)
+ Gitlab::Database.bulk_insert(table, data) # rubocop:disable Gitlab/BulkInsert
end
return if migrated_ids.empty?
diff --git a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb
index 35bfc381180..fcbcaacb2d6 100644
--- a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb
+++ b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb
@@ -73,7 +73,7 @@ module Gitlab
end
def insert_into_cluster_kubernetes_namespace(rows)
- Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name,
+ Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, # rubocop:disable Gitlab/BulkInsert
rows,
disable_quote: [:created_at, :updated_at])
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index d2924d10225..43698b7955f 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -95,7 +95,7 @@ module Gitlab
file.to_h.merge(created_at: 'NOW()')
end
- Gitlab::Database.bulk_insert('uploads',
+ Gitlab::Database.bulk_insert('uploads', # rubocop:disable Gitlab/BulkInsert
rows,
disable_quote: :created_at)
end
diff --git a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb
index cf0f582a2d4..d71a50a0af6 100644
--- a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb
+++ b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb
@@ -25,7 +25,7 @@ module Gitlab
mentions << mention_record unless mention_record.blank?
end
- Gitlab::Database.bulk_insert(
+ Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
resource_user_mention_model.table_name,
mentions,
return_ids: true,
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 5174aed04ba..21aace9f67a 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.dast-auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
dast_environment_deploy:
extends: .dast-auto-deploy
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 1e85aad02d9..381b116dacb 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
include:
- template: Jobs/Deploy/ECS.gitlab-ci.yml
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 008baf8ae0d..f2074d86bd7 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -12,8 +12,8 @@
#
# To enable the experiment for 10% of the users:
#
-# chatops: `/chatops run feature set experiment_key_experiment_percentage 10 --actors`
-# console: `Feature.enable_percentage_of_actors(:experiment_key_experiment_percentage, 10)`
+# chatops: `/chatops run feature set experiment_key_experiment_percentage 10`
+# console: `Feature.enable_percentage_of_time(:experiment_key_experiment_percentage, 10)`
#
# To disable the experiment:
#
@@ -26,7 +26,7 @@
# console: `Feature.get(:experiment_key_experiment_percentage).percentage_of_time_value`
#
-# TODO: rewrite that
+# TODO: see https://gitlab.com/gitlab-org/gitlab/-/issues/217490
module Gitlab
module Experimentation
EXPERIMENTS = {
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
index 147597289cf..0d448b55104 100644
--- a/lib/gitlab/github_import/bulk_importing.rb
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -17,7 +17,7 @@ module Gitlab
# Bulk inserts the given rows into the database.
def bulk_insert(model, rows, batch_size: 100)
rows.each_slice(batch_size) do |slice|
- Gitlab::Database.bulk_insert(model.table_name, slice)
+ Gitlab::Database.bulk_insert(model.table_name, slice) # rubocop:disable Gitlab/BulkInsert
end
end
end
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
index d562958e955..53b17f77ccd 100644
--- a/lib/gitlab/github_import/importer/diff_note_importer.rb
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -47,7 +47,7 @@ module Gitlab
# To work around this we're using bulk_insert with a single row. This
# allows us to efficiently insert data (even if it's just 1 row)
# without having to use all sorts of hacks to disable callbacks.
- Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes])
+ Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert
rescue ActiveRecord::InvalidForeignKey
# It's possible the project and the issue have been deleted since
# scheduling this job. In this case we'll just skip creating the note.
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 8648cbaec9d..13061d2c9df 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -75,7 +75,7 @@ module Gitlab
end
end
- Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees)
+ Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees) # rubocop:disable Gitlab/BulkInsert
end
end
end
diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb
index 2001b7e3482..77eb4542195 100644
--- a/lib/gitlab/github_import/importer/label_links_importer.rb
+++ b/lib/gitlab/github_import/importer/label_links_importer.rb
@@ -40,7 +40,7 @@ module Gitlab
}
end
- Gitlab::Database.bulk_insert(LabelLink.table_name, rows)
+ Gitlab::Database.bulk_insert(LabelLink.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
def find_target_id
diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
index 2b06d1b3baf..41f179d275b 100644
--- a/lib/gitlab/github_import/importer/note_importer.rb
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -38,7 +38,7 @@ module Gitlab
# We're using bulk_insert here so we can bypass any validations and
# callbacks. Running these would result in a lot of unnecessary SQL
# queries being executed when importing large projects.
- Gitlab::Database.bulk_insert(Note.table_name, [attributes])
+ Gitlab::Database.bulk_insert(Note.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert
rescue ActiveRecord::InvalidForeignKey
# It's possible the project and the issue have been deleted since
# scheduling this job. In this case we'll just skip creating the note.
diff --git a/lib/gitlab/import/database_helpers.rb b/lib/gitlab/import/database_helpers.rb
index aaade39dd62..f8ea7a7adcd 100644
--- a/lib/gitlab/import/database_helpers.rb
+++ b/lib/gitlab/import/database_helpers.rb
@@ -11,7 +11,7 @@ module Gitlab
# We use bulk_insert here so we can bypass any queries executed by
# callbacks or validation rules, as doing this wouldn't scale when
# importing very large projects.
- result = Gitlab::Database
+ result = Gitlab::Database # rubocop:disable Gitlab/BulkInsert
.bulk_insert(relation.table_name, [attributes], return_ids: true)
result.first
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 62f1c4ed0e8..0fe0ae1d4b6 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -181,6 +181,7 @@ module Gitlab
def features_usage_data_ce
{
+ instance_auto_devops_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.auto_devops_enabled? },
container_registry_enabled: alt_usage_data(fallback: nil) { Gitlab.config.registry.enabled },
dependency_proxy_enabled: Gitlab.config.try(:dependency_proxy)&.enabled,
gitlab_shared_runners_enabled: alt_usage_data(fallback: nil) { Gitlab.config.gitlab_ci.shared_runners_enabled },
diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb
index b3c0e68dbb3..5eab882039d 100644
--- a/lib/object_storage/direct_upload.rb
+++ b/lib/object_storage/direct_upload.rb
@@ -46,7 +46,7 @@ module ObjectStorage
MultipartUpload: multipart_upload_hash,
CustomPutHeaders: true,
PutHeaders: upload_options
- }.compact
+ }.merge(workhorse_client_hash).compact
end
def multipart_upload_hash
@@ -60,6 +60,32 @@ module ObjectStorage
}
end
+ def workhorse_client_hash
+ return {} unless aws?
+
+ {
+ UseWorkhorseClient: use_workhorse_s3_client?,
+ RemoteTempObjectID: object_name,
+ ObjectStorage: {
+ Provider: 'AWS',
+ S3Config: {
+ Bucket: bucket_name,
+ Region: credentials[:region],
+ Endpoint: credentials[:endpoint],
+ PathStyle: credentials.fetch(:path_style, false),
+ UseIamProfile: credentials.fetch(:use_iam_profile, false)
+ }
+ }
+ }
+ end
+
+ def use_workhorse_s3_client?
+ Feature.enabled?(:use_workhorse_s3_client, default_enabled: true) &&
+ credentials.fetch(:use_iam_profile, false) &&
+ # The Golang AWS SDK does not support V2 signatures
+ credentials.fetch(:aws_signature_version, 4).to_i >= 4
+ end
+
def provider
credentials[:provider].to_s
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 03d7a71a90c..91abc1fd892 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6575,6 +6575,9 @@ msgstr ""
msgid "Created by me"
msgstr ""
+msgid "Created date"
+msgstr ""
+
msgid "Created issue %{issueLink}"
msgstr ""
@@ -13168,6 +13171,9 @@ msgstr ""
msgid "Loading…"
msgstr ""
+msgid "Local IP addresses and domain names that hooks and services may access."
+msgstr ""
+
msgid "Localization"
msgstr ""
@@ -15546,10 +15552,10 @@ msgstr ""
msgid "PackageRegistry|Upcoming package managers"
msgstr ""
-msgid "PackageRegistry|You are about to delete <b>%{packageName}</b>, this operation is irreversible, are you sure?"
+msgid "PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?"
msgstr ""
-msgid "PackageRegistry|You are about to delete version %{boldStart}%{version}%{boldEnd} of %{boldStart}%{name}%{boldEnd}. Are you sure?"
+msgid "PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?"
msgstr ""
msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
@@ -18597,7 +18603,7 @@ msgstr ""
msgid "Requests Profiles"
msgstr ""
-msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com."
+msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The allowlist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com."
msgstr ""
msgid "Require all users in this group to setup Two-factor authentication"
@@ -19167,6 +19173,9 @@ msgstr ""
msgid "Search projects..."
msgstr ""
+msgid "Search requirements"
+msgstr ""
+
msgid "Search users"
msgstr ""
@@ -25000,9 +25009,6 @@ msgstr ""
msgid "White helpers give contextual information."
msgstr ""
-msgid "Whitelist to allow requests to the local network from hooks and services"
-msgstr ""
-
msgid "Who can be an approver?"
msgstr ""
diff --git a/rubocop/cop/gitlab/bulk_insert.rb b/rubocop/cop/gitlab/bulk_insert.rb
new file mode 100644
index 00000000000..c03ffbe0b2a
--- /dev/null
+++ b/rubocop/cop/gitlab/bulk_insert.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Gitlab
+ # Cop that disallows the use of `Gitlab::Database.bulk_insert`, in favour of using
+ # the `BulkInsertSafe` module.
+ class BulkInsert < RuboCop::Cop::Cop
+ MSG = 'Use the `BulkInsertSafe` concern, instead of using `Gitlab::Database.bulk_insert`. See https://docs.gitlab.com/ee/development/insert_into_tables_in_batches.html'
+
+ def_node_matcher :raw_union?, <<~PATTERN
+ (send (const (const nil? :Gitlab) :Database) :bulk_insert ...)
+ PATTERN
+
+ def on_send(node)
+ return unless raw_union?(node)
+
+ add_offense(node, location: :expression)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
index 3b9a3dded1e..c0ce934a0f1 100644
--- a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
+++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
@@ -9,21 +9,8 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
let(:param_type) { 'coverage' }
let(:start_date) { '2019-12-10' }
let(:end_date) { '2020-03-09' }
-
- def create_daily_coverage(group_name, coverage, date)
- create(
- :ci_daily_build_group_report_result,
- project: project,
- ref_path: ref_path,
- group_name: group_name,
- data: { 'coverage' => coverage },
- date: date
- )
- end
-
- def csv_response
- CSV.parse(response.body)
- end
+ let(:allowed_to_read) { true }
+ let(:user) { create(:user) }
before do
create_daily_coverage('rspec', 79.0, '2020-03-09')
@@ -31,6 +18,11 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
create_daily_coverage('rspec', 67.0, '2019-12-09')
create_daily_coverage('karma', 71.0, '2019-12-09')
+ sign_in(user)
+
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_build_report_results, project).and_return(allowed_to_read)
+
get :index, params: {
namespace_id: project.namespace,
project_id: project,
@@ -76,5 +68,28 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
+
+ context 'when user is not allowed to read build report results' do
+ let(:allowed_to_read) { false }
+
+ it 'responds with 404 error' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ def create_daily_coverage(group_name, coverage, date)
+ create(
+ :ci_daily_build_group_report_result,
+ project: project,
+ ref_path: ref_path,
+ group_name: group_name,
+ data: { 'coverage' => coverage },
+ date: date
+ )
+ end
+
+ def csv_response
+ CSV.parse(response.body)
end
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 140af74b576..186b935a49a 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -42,23 +42,37 @@ RSpec.describe Projects::GraphsController do
expect(response).to render_template(:charts)
end
- it 'sets the daily coverage options' do
- Timecop.freeze do
+ context 'when anonymous users can read build report results' do
+ it 'sets the daily coverage options' do
+ Timecop.freeze do
+ get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
+
+ expect(assigns[:daily_coverage_options]).to eq(
+ base_params: {
+ start_date: Time.current.to_date - 90.days,
+ end_date: Time.current.to_date,
+ ref_path: project.repository.expand_ref('master'),
+ param_type: 'coverage'
+ },
+ download_path: namespace_project_ci_daily_build_group_report_results_path(
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :csv
+ )
+ )
+ end
+ end
+ end
+
+ context 'when anonymous users cannot read build report results' do
+ before do
+ project.update_column(:public_builds, false)
+
get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
+ end
- expect(assigns[:daily_coverage_options]).to eq(
- base_params: {
- start_date: Time.current.to_date - 90.days,
- end_date: Time.current.to_date,
- ref_path: project.repository.expand_ref('master'),
- param_type: 'coverage'
- },
- download_path: namespace_project_ci_daily_build_group_report_results_path(
- namespace_id: project.namespace,
- project_id: project,
- format: :csv
- )
- )
+ it 'does not set daily coverage options' do
+ expect(assigns[:daily_coverage_options]).to be_nil
end
end
end
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
index 59d4cc56f95..6d1229063d8 100644
--- a/spec/factories/design_management/designs.rb
+++ b/spec/factories/design_management/designs.rb
@@ -35,7 +35,7 @@ FactoryBot.define do
sha = commit_version[action]
version = DesignManagement::Version.new(sha: sha, issue: issue, author: evaluator.author)
version.save(validate: false) # We need it to have an ID, validate later
- Gitlab::Database.bulk_insert(dv_table_name, [action.row_attrs(version)])
+ Gitlab::Database.bulk_insert(dv_table_name, [action.row_attrs(version)]) # rubocop:disable Gitlab/BulkInsert
end
# always a creation
diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
index 3000ef650d3..e7b11bef787 100644
--- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
+++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
@@ -8,17 +8,6 @@ describe Ci::DailyBuildGroupReportResultsFinder do
let(:ref_path) { 'refs/heads/master' }
let(:limit) { nil }
- def create_daily_coverage(group_name, coverage, date)
- create(
- :ci_daily_build_group_report_result,
- project: project,
- ref_path: ref_path,
- group_name: group_name,
- data: { 'coverage' => coverage },
- date: date
- )
- end
-
let!(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') }
let!(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') }
let!(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') }
@@ -37,7 +26,7 @@ describe Ci::DailyBuildGroupReportResultsFinder do
).execute
end
- context 'when current user is allowed to download project code' do
+ context 'when current user is allowed to read build report results' do
let(:current_user) { project.owner }
it 'returns all matching results within the given date range' do
@@ -61,7 +50,7 @@ describe Ci::DailyBuildGroupReportResultsFinder do
end
end
- context 'when current user is not allowed to download project code' do
+ context 'when current user is not allowed to read build report results' do
let(:current_user) { create(:user) }
it 'returns an empty result' do
@@ -69,4 +58,15 @@ describe Ci::DailyBuildGroupReportResultsFinder do
end
end
end
+
+ def create_daily_coverage(group_name, coverage, date)
+ create(
+ :ci_daily_build_group_report_result,
+ project: project,
+ ref_path: ref_path,
+ group_name: group_name,
+ data: { 'coverage' => coverage },
+ date: date
+ )
+ end
end
diff --git a/spec/fixtures/packages/conan/package_files/conan_package.tgz b/spec/fixtures/packages/conan/package_files/conan_package.tgz
new file mode 100644
index 00000000000..6163364f3f9
--- /dev/null
+++ b/spec/fixtures/packages/conan/package_files/conan_package.tgz
Binary files differ
diff --git a/spec/fixtures/packages/conan/package_files/conaninfo.txt b/spec/fixtures/packages/conan/package_files/conaninfo.txt
new file mode 100644
index 00000000000..2a02515a19b
--- /dev/null
+++ b/spec/fixtures/packages/conan/package_files/conaninfo.txt
@@ -0,0 +1,33 @@
+[settings]
+ arch=x86_64
+ build_type=Release
+ compiler=apple-clang
+ compiler.libcxx=libc++
+ compiler.version=10.0
+ os=Macos
+
+[requires]
+
+
+[options]
+ shared=False
+
+[full_settings]
+ arch=x86_64
+ build_type=Release
+ compiler=apple-clang
+ compiler.libcxx=libc++
+ compiler.version=10.0
+ os=Macos
+
+[full_requires]
+
+
+[full_options]
+ shared=False
+
+[recipe_hash]
+ b4b91125b36b40a7076a98310588f820
+
+[env]
+
diff --git a/spec/fixtures/packages/conan/package_files/conanmanifest.txt b/spec/fixtures/packages/conan/package_files/conanmanifest.txt
new file mode 100644
index 00000000000..bc34b81b050
--- /dev/null
+++ b/spec/fixtures/packages/conan/package_files/conanmanifest.txt
@@ -0,0 +1,4 @@
+1565723794
+conaninfo.txt: 2774ebe649804c1cd9430f26ab0ead14
+include/hello.h: 8727846905bd09baecf8bdc1edb1f46e
+lib/libhello.a: 7f2aaa8b6f3bc316bba59e47b6a0bd43
diff --git a/spec/fixtures/packages/conan/recipe_files/conanfile.py b/spec/fixtures/packages/conan/recipe_files/conanfile.py
new file mode 100644
index 00000000000..910bd5a0b51
--- /dev/null
+++ b/spec/fixtures/packages/conan/recipe_files/conanfile.py
@@ -0,0 +1,47 @@
+from conans import ConanFile, CMake, tools
+
+
+class HelloConan(ConanFile):
+ name = "Hello"
+ version = "0.1"
+ license = "<Put the package license here>"
+ author = "<Put your name here> <And your email here>"
+ url = "<Package recipe repository url here, for issues about the package>"
+ description = "<Description of Hello here>"
+ topics = ("<Put some tag here>", "<here>", "<and here>")
+ settings = "os", "compiler", "build_type", "arch"
+ options = {"shared": [True, False]}
+ default_options = "shared=False"
+ generators = "cmake"
+
+ def source(self):
+ self.run("git clone https://github.com/conan-io/hello.git")
+ # This small hack might be useful to guarantee proper /MT /MD linkage
+ # in MSVC if the packaged project doesn't have variables to set it
+ # properly
+ tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(HelloWorld)",
+ '''PROJECT(HelloWorld)
+include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
+conan_basic_setup()''')
+
+ def build(self):
+ cmake = CMake(self)
+ cmake.configure(source_folder="hello")
+ cmake.build()
+
+ # Explicit way:
+ # self.run('cmake %s/hello %s'
+ # % (self.source_folder, cmake.command_line))
+ # self.run("cmake --build . %s" % cmake.build_config)
+
+ def package(self):
+ self.copy("*.h", dst="include", src="hello")
+ self.copy("*hello.lib", dst="lib", keep_path=False)
+ self.copy("*.dll", dst="bin", keep_path=False)
+ self.copy("*.so", dst="lib", keep_path=False)
+ self.copy("*.dylib", dst="lib", keep_path=False)
+ self.copy("*.a", dst="lib", keep_path=False)
+
+ def package_info(self):
+ self.cpp_info.libs = ["hello"]
+
diff --git a/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt b/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt
new file mode 100644
index 00000000000..432b12f39fa
--- /dev/null
+++ b/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt
@@ -0,0 +1,2 @@
+1565723790
+conanfile.py: 7c042b95312cc4c4ee89199dc51aebf9
diff --git a/spec/fixtures/packages/maven/maven-metadata.xml b/spec/fixtures/packages/maven/maven-metadata.xml
new file mode 100644
index 00000000000..7d7549df227
--- /dev/null
+++ b/spec/fixtures/packages/maven/maven-metadata.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata modelVersion="1.1.0">
+ <groupId>com.mycompany.app</groupId>
+ <artifactId>my-app</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <versioning>
+ <snapshot>
+ <timestamp>20180724.124855</timestamp>
+ <buildNumber>1</buildNumber>
+ </snapshot>
+ <lastUpdated>20180724124855</lastUpdated>
+ <snapshotVersions>
+ <snapshotVersion>
+ <extension>jar</extension>
+ <value>1.0-20180724.124855-1</value>
+ <updated>20180724124855</updated>
+ </snapshotVersion>
+ <snapshotVersion>
+ <extension>pom</extension>
+ <value>1.0-20180724.124855-1</value>
+ <updated>20180724124855</updated>
+ </snapshotVersion>
+ </snapshotVersions>
+ </versioning>
+</metadata>
diff --git a/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar
new file mode 100644
index 00000000000..ea3903cf6d9
--- /dev/null
+++ b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar
Binary files differ
diff --git a/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom
new file mode 100644
index 00000000000..6b6015314aa
--- /dev/null
+++ b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom
@@ -0,0 +1,34 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.mycompany.app</groupId>
+ <artifactId>my-app</artifactId>
+ <packaging>jar</packaging>
+ <version>1.0-SNAPSHOT</version>
+ <name>my-app</name>
+ <url>http://maven.apache.org</url>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <distributionManagement>
+ <snapshotRepository>
+ <id>local</id>
+ <url>file:///tmp/maven</url>
+ </snapshotRepository>
+ </distributionManagement>
+ <repositories>
+ <repository>
+ <id>local</id>
+ <url>file:///tmp/maven</url>
+ </repository>
+ </repositories>
+ <properties>
+ <maven.compiler.source>1.6</maven.compiler.source>
+ <maven.compiler.target>1.6</maven.compiler.target>
+ </properties>
+</project>
diff --git a/spec/fixtures/packages/npm/foo-1.0.1.tgz b/spec/fixtures/packages/npm/foo-1.0.1.tgz
new file mode 100644
index 00000000000..a2bcdb8d492
--- /dev/null
+++ b/spec/fixtures/packages/npm/foo-1.0.1.tgz
Binary files differ
diff --git a/spec/fixtures/packages/npm/payload.json b/spec/fixtures/packages/npm/payload.json
new file mode 100644
index 00000000000..664aa636001
--- /dev/null
+++ b/spec/fixtures/packages/npm/payload.json
@@ -0,0 +1,30 @@
+{
+ "_id":"@root/npm-test",
+ "name":"@root/npm-test",
+ "description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ "dist-tags":{
+ "latest":"1.0.1"
+ },
+ "versions":{
+ "1.0.1":{
+ "name":"@root/npm-test",
+ "version":"1.0.1",
+ "main":"app.js",
+ "dependencies":{
+ "express":"^4.16.4"
+ },
+ "dist":{
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ }
+ }
+ },
+ "_attachments":{
+ "@root/npm-test-1.0.1.tgz":{
+ "content_type":"application/octet-stream",
+ "data":"aGVsbG8K",
+ "length":8
+ }
+ },
+ "id":"10",
+ "package_name":"@root/npm-test"
+}
diff --git a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
new file mode 100644
index 00000000000..a6ea8760bd5
--- /dev/null
+++ b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
@@ -0,0 +1,44 @@
+{
+ "_id":"@root/npm-test",
+ "name":"@root/npm-test",
+ "description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ "dist-tags":{
+ "latest":"1.0.1"
+ },
+ "versions":{
+ "1.0.1":{
+ "name":"@root/npm-test",
+ "version":"1.0.1",
+ "main":"app.js",
+ "dependencies":{
+ "express":"^4.16.4",
+ "dagre-d3": "~0.3.2"
+ },
+ "devDependencies": {
+ "dagre-d3": "~0.3.2",
+ "d3": "~3.4.13"
+ },
+ "bundleDependencies": {
+ "d3": "~3.4.13"
+ },
+ "peerDependencies": {
+ "d3": "~3.3.0"
+ },
+ "deprecated": {
+ "express":"^4.16.4"
+ },
+ "dist":{
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ }
+ }
+ },
+ "_attachments":{
+ "@root/npm-test-1.0.1.tgz":{
+ "content_type":"application/octet-stream",
+ "data":"aGVsbG8K",
+ "length":8
+ }
+ },
+ "id":"10",
+ "package_name":"@root/npm-test"
+}
diff --git a/spec/fixtures/packages/nuget/package.nupkg b/spec/fixtures/packages/nuget/package.nupkg
new file mode 100644
index 00000000000..b36856ee569
--- /dev/null
+++ b/spec/fixtures/packages/nuget/package.nupkg
Binary files differ
diff --git a/spec/fixtures/packages/nuget/with_dependencies.nuspec b/spec/fixtures/packages/nuget/with_dependencies.nuspec
new file mode 100644
index 00000000000..753037cd05b
--- /dev/null
+++ b/spec/fixtures/packages/nuget/with_dependencies.nuspec
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>Test.Package</id>
+ <version>3.5.2</version>
+ <authors>Test Author</authors>
+ <owners>Test Owner</owners>
+ <requireLicenseAcceptance>false</requireLicenseAcceptance>
+ <description>Package Description</description>
+ <dependencies>
+ <dependency id="Moqi" version="2.5.6" include="Runtime,Compile" />
+ <group targetFramework=".NETStandard2.0">
+ <dependency id="Test.Dependency" version="2.3.7" exclude="Build,Analyzers" include="Runtime,Compile" />
+ <dependency id="Newtonsoft.Json" version="12.0.3" exclude="Build,Analyzers" />
+ </group>
+ <dependency id="Castle.Core" />
+ </dependencies>
+ </metadata>
+</package>
diff --git a/spec/fixtures/packages/nuget/with_metadata.nuspec b/spec/fixtures/packages/nuget/with_metadata.nuspec
new file mode 100644
index 00000000000..0043bc89527
--- /dev/null
+++ b/spec/fixtures/packages/nuget/with_metadata.nuspec
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>DummyProject.WithMetadata</id>
+ <version>1.2.3</version>
+ <title>nuspec with metadata</title>
+ <authors>Author Test</authors>
+ <owners>Author Test</owners>
+ <developmentDependency>true</developmentDependency>
+ <requireLicenseAcceptance>true</requireLicenseAcceptance>
+ <licenseUrl>https://opensource.org/licenses/MIT</licenseUrl>
+ <projectUrl>https://gitlab.com/gitlab-org/gitlab</projectUrl>
+ <iconUrl>https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png</iconUrl>
+ <description>Description Test</description>
+ <releaseNotes>Release Notes Test</releaseNotes>
+ <copyright>Copyright Test</copyright>
+ <tags>foo bar test tag1 tag2 tag3 tag4 tag5</tags>
+ </metadata>
+</package>
diff --git a/spec/fixtures/packages/pypi/sample-project.tar.gz b/spec/fixtures/packages/pypi/sample-project.tar.gz
new file mode 100644
index 00000000000..c71b1fef23d
--- /dev/null
+++ b/spec/fixtures/packages/pypi/sample-project.tar.gz
Binary files differ
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 62a59a76bf4..da17cc3601e 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -120,6 +120,46 @@ describe('new file modal component', () => {
});
});
+ describe('createFromTemplate', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.entries = {
+ 'test-path/test': {
+ name: 'test',
+ deleted: false,
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ vm.open('blob');
+
+ jest.spyOn(vm, 'createTempEntry').mockImplementation();
+ });
+
+ it.each`
+ entryName | newFilePath
+ ${''} | ${'.gitignore'}
+ ${'README.md'} | ${'.gitignore'}
+ ${'test-path/test/'} | ${'test-path/test/.gitignore'}
+ ${'test-path/test'} | ${'test-path/.gitignore'}
+ ${'test-path/test/abc.md'} | ${'test-path/test/.gitignore'}
+ `(
+ 'creates a new file with the given template name in appropriate directory for path: $path',
+ ({ entryName, newFilePath }) => {
+ vm.entryName = entryName;
+
+ vm.createFromTemplate({ name: '.gitignore' });
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: newFilePath,
+ type: 'blob',
+ });
+ },
+ );
+ });
+
describe('submitForm', () => {
let store;
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index a6f712b1984..017461dfb84 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -1,5 +1,7 @@
import { mount } from '@vue/test-utils';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
+import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants';
+import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils';
import { parsedData } from './mock_data';
@@ -8,8 +10,8 @@ describe('The DAG graph', () => {
let wrapper;
const getGraph = () => wrapper.find('.dag-graph-container > svg');
- const getAllLinks = () => wrapper.findAll('.dag-link');
- const getAllNodes = () => wrapper.findAll('.dag-node');
+ const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`);
+ const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`);
const getAllLabels = () => wrapper.findAll('foreignObject');
const createComponent = (propsData = {}) => {
@@ -94,4 +96,123 @@ describe('The DAG graph', () => {
});
});
});
+
+ describe('interactions', () => {
+ const strokeOpacity = opacity => `stroke-opacity: ${opacity};`;
+ const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity;
+
+ describe('links', () => {
+ const liveLink = () => getAllLinks().at(4);
+ const otherLink = () => getAllLinks().at(1);
+
+ describe('on hover', () => {
+ it('sets the link opacity to baseOpacity and background links to 0.2', () => {
+ liveLink().trigger('mouseover');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+ });
+
+ it('reverts the styles on mouseout', () => {
+ liveLink().trigger('mouseover');
+ liveLink().trigger('mouseout');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ });
+ });
+
+ describe('on click', () => {
+ describe('toggles link liveness', () => {
+ it('turns link on', () => {
+ liveLink().trigger('click');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+ });
+
+ it('turns link off on second click', () => {
+ liveLink().trigger('click');
+ liveLink().trigger('click');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ });
+ });
+
+ it('the link remains live even after mouseout', () => {
+ liveLink().trigger('click');
+ liveLink().trigger('mouseout');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+ });
+
+ it('preserves state when multiple links are toggled on and off', () => {
+ const anotherLiveLink = () => getAllLinks().at(2);
+
+ liveLink().trigger('click');
+ anotherLiveLink().trigger('click');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+
+ anotherLiveLink().trigger('click');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
+ expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
+
+ liveLink().trigger('click');
+ expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
+ });
+ });
+ });
+
+ describe('nodes', () => {
+ const liveNode = () => getAllNodes().at(10);
+ const anotherLiveNode = () => getAllNodes().at(5);
+ const nodesNotHighlighted = () => getAllNodes().filter(n => !n.classes(IS_HIGHLIGHTED));
+ const linksNotHighlighted = () => getAllLinks().filter(n => !n.classes(IS_HIGHLIGHTED));
+ const nodesHighlighted = () => getAllNodes().filter(n => n.classes(IS_HIGHLIGHTED));
+ const linksHighlighted = () => getAllLinks().filter(n => n.classes(IS_HIGHLIGHTED));
+
+ describe('on click', () => {
+ it('highlights the clicked node and predecessors', () => {
+ liveNode().trigger('click');
+
+ expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
+ expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
+
+ linksHighlighted().wrappers.forEach(link => {
+ expect(link.attributes('style')).toBe(strokeOpacity(highlightIn));
+ });
+
+ nodesHighlighted().wrappers.forEach(node => {
+ expect(node.attributes('stroke')).not.toBe('#f2f2f2');
+ });
+
+ linksNotHighlighted().wrappers.forEach(link => {
+ expect(link.attributes('style')).toBe(strokeOpacity(highlightOut));
+ });
+
+ nodesNotHighlighted().wrappers.forEach(node => {
+ expect(node.attributes('stroke')).toBe('#f2f2f2');
+ });
+ });
+
+ it('toggles path off on second click', () => {
+ liveNode().trigger('click');
+ liveNode().trigger('click');
+
+ expect(nodesNotHighlighted().length).toBe(getAllNodes().length);
+ expect(linksNotHighlighted().length).toBe(getAllLinks().length);
+ });
+
+ it('preserves state when multiple nodes are toggled on and off', () => {
+ anotherLiveNode().trigger('click');
+ liveNode().trigger('click');
+ anotherLiveNode().trigger('click');
+ expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
+ expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
new file mode 100644
index 00000000000..6ef4dcf96b4
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
@@ -0,0 +1,111 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import component from '~/registry/explorer/components/details_page/delete_alert.vue';
+import {
+ DELETE_TAG_SUCCESS_MESSAGE,
+ DELETE_TAG_ERROR_MESSAGE,
+ DELETE_TAGS_SUCCESS_MESSAGE,
+ DELETE_TAGS_ERROR_MESSAGE,
+ ADMIN_GARBAGE_COLLECTION_TIP,
+} from '~/registry/explorer/constants';
+
+describe('Delete alert', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.find(GlAlert);
+ const findLink = () => wrapper.find(GlLink);
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
+ };
+
+ describe('when deleteAlertType is null', () => {
+ it('does not show the alert', () => {
+ mountComponent();
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when deleteAlertType is not null', () => {
+ describe('success states', () => {
+ describe.each`
+ deleteAlertType | message
+ ${'success_tag'} | ${DELETE_TAG_SUCCESS_MESSAGE}
+ ${'success_tags'} | ${DELETE_TAGS_SUCCESS_MESSAGE}
+ `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => {
+ it('alert exists', () => {
+ mountComponent({ deleteAlertType });
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('when the user is an admin', () => {
+ beforeEach(() => {
+ mountComponent({
+ deleteAlertType,
+ isAdmin: true,
+ garbageCollectionHelpPagePath: 'foo',
+ });
+ });
+
+ it(`alert title is ${message}`, () => {
+ expect(findAlert().attributes('title')).toBe(message);
+ });
+
+ it('alert body contains admin tip', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(ADMIN_GARBAGE_COLLECTION_TIP);
+ });
+
+ it('alert body contains link', () => {
+ const alertLink = findLink();
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.attributes('href')).toBe('foo');
+ });
+ });
+
+ describe('when the user is not an admin', () => {
+ it('alert exist and text is appropriate', () => {
+ mountComponent({ deleteAlertType });
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(message);
+ });
+ });
+ });
+ });
+ describe('error states', () => {
+ describe.each`
+ deleteAlertType | message
+ ${'danger_tag'} | ${DELETE_TAG_ERROR_MESSAGE}
+ ${'danger_tags'} | ${DELETE_TAGS_ERROR_MESSAGE}
+ `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => {
+ it('alert exists', () => {
+ mountComponent({ deleteAlertType });
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('when the user is an admin', () => {
+ it('alert exist and text is appropriate', () => {
+ mountComponent({ deleteAlertType });
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(message);
+ });
+ });
+
+ describe('when the user is not an admin', () => {
+ it('alert exist and text is appropriate', () => {
+ mountComponent({ deleteAlertType });
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(message);
+ });
+ });
+ });
+ });
+
+ describe('dismissing alert', () => {
+ it('GlAlert dismiss event triggers a change event', () => {
+ mountComponent({ deleteAlertType: 'success_tags' });
+ findAlert().vm.$emit('dismiss');
+ expect(wrapper.emitted('change')).toEqual([[null]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
new file mode 100644
index 00000000000..b30818005c3
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
@@ -0,0 +1,74 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import component from '~/registry/explorer/components/details_page/delete_modal.vue';
+import {
+ REMOVE_TAG_CONFIRMATION_TEXT,
+ REMOVE_TAGS_CONFIRMATION_TEXT,
+} from '~/registry/explorer/constants';
+import { GlModal } from '../../stubs';
+
+describe('Delete Modal', () => {
+ let wrapper;
+
+ const findModal = () => wrapper.find(GlModal);
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ GlModal,
+ },
+ });
+ };
+
+ it('contains a GlModal', () => {
+ mountComponent();
+ expect(findModal().exists()).toBe(true);
+ });
+
+ describe('events', () => {
+ it.each`
+ glEvent | localEvent
+ ${'ok'} | ${'confirmDelete'}
+ ${'cancel'} | ${'cancelDelete'}
+ `('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => {
+ mountComponent();
+ findModal().vm.$emit(glEvent);
+ expect(wrapper.emitted(localEvent)).toBeTruthy();
+ });
+ });
+
+ describe('methods', () => {
+ it('show calls gl-modal show', () => {
+ mountComponent();
+ wrapper.vm.show();
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ });
+ });
+
+ describe('itemsToBeDeleted contains one element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
+ });
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'));
+ });
+ it('has the correct action', () => {
+ expect(wrapper.text()).toContain('Remove tag');
+ });
+ });
+
+ describe('itemsToBeDeleted contains more than element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
+ });
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'));
+ });
+ it('has the correct action', () => {
+ expect(wrapper.text()).toContain('Remove tags');
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
new file mode 100644
index 00000000000..ad2b8a12ecc
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import component from '~/registry/explorer/components/details_page/details_header.vue';
+import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants';
+
+describe('Details Header', () => {
+ let wrapper;
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ it('has the correct title ', () => {
+ mountComponent();
+ expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
+ });
+
+ it('shows imageName in the title', () => {
+ mountComponent({ imageName: 'foo' });
+ expect(wrapper.text()).toContain('foo');
+ });
+});
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index f6beccda9b1..e2b33826503 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -64,7 +64,7 @@ export const imagesListResponse = {
export const tagsListResponse = {
data: [
{
- tag: 'centos6',
+ name: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
short_revision: 'b118ab5b0',
size: 19,
@@ -75,7 +75,7 @@ export const tagsListResponse = {
destroy_path: 'path',
},
{
- tag: 'test-image',
+ name: 'test-tag',
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
short_revision: 'b969de599',
size: 19,
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 93098403a28..6fa4448083a 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -1,24 +1,20 @@
import { mount } from '@vue/test-utils';
-import { GlTable, GlPagination, GlSkeletonLoader, GlAlert, GlLink } from '@gitlab/ui';
+import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue';
+import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
+import DeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
+import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import { createStore } from '~/registry/explorer/stores/';
import {
SET_MAIN_LOADING,
- SET_INITIAL_STATE,
SET_TAGS_LIST_SUCCESS,
SET_TAGS_PAGINATION,
+ SET_INITIAL_STATE,
} from '~/registry/explorer/stores/mutation_types/';
-import {
- DELETE_TAG_SUCCESS_MESSAGE,
- DELETE_TAG_ERROR_MESSAGE,
- DELETE_TAGS_SUCCESS_MESSAGE,
- DELETE_TAGS_ERROR_MESSAGE,
- ADMIN_GARBAGE_COLLECTION_TIP,
-} from '~/registry/explorer/constants';
+
import { tagsListResponse } from '../mock_data';
-import { GlModal } from '../stubs';
import { $toast } from '../../shared/mocks';
describe('Details Page', () => {
@@ -26,7 +22,7 @@ describe('Details Page', () => {
let dispatchSpy;
let store;
- const findDeleteModal = () => wrapper.find(GlModal);
+ const findDeleteModal = () => wrapper.find(DeleteModal);
const findPagination = () => wrapper.find(GlPagination);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
@@ -38,7 +34,8 @@ describe('Details Page', () => {
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
- const findAlert = () => wrapper.find(GlAlert);
+ const findDeleteAlert = () => wrapper.find(DeleteAlert);
+ const findDetailsHeader = () => wrapper.find(DetailsHeader);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
@@ -47,9 +44,9 @@ describe('Details Page', () => {
store,
stubs: {
...stubChildren(component),
- GlModal,
GlSprintf: false,
GlTable,
+ DeleteModal,
},
mocks: {
$route: {
@@ -70,6 +67,7 @@ describe('Details Page', () => {
store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
jest.spyOn(Tracking, 'event');
+ jest.spyOn(DeleteModal.methods, 'show');
});
afterEach(() => {
@@ -100,10 +98,6 @@ describe('Details Page', () => {
});
describe('table', () => {
- beforeEach(() => {
- mountComponent();
- });
-
it.each([
'rowCheckbox',
'rowName',
@@ -112,6 +106,7 @@ describe('Details Page', () => {
'rowTime',
'singleDeleteButton',
])('%s exist in the table', element => {
+ mountComponent();
expect(findFirstRowItem(element).exists()).toBe(true);
});
@@ -143,16 +138,20 @@ describe('Details Page', () => {
});
describe('row checkbox', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('if selected adds item to selectedItems', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.selectedItems).toEqual([1]);
+ expect(wrapper.vm.selectedItems).toEqual([store.state.tags[1].name]);
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy();
});
});
- it('if deselect remove index from selectedItems', () => {
- wrapper.setData({ selectedItems: [1] });
+ it('if deselect remove name from selectedItems', () => {
+ wrapper.setData({ selectedItems: [store.state.tags[1].name] });
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectedItems.length).toBe(0);
@@ -167,14 +166,17 @@ describe('Details Page', () => {
});
it('exists', () => {
+ mountComponent();
expect(findBulkDeleteButton().exists()).toBe(true);
});
it('is disabled if no item is selected', () => {
+ mountComponent();
expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
});
it('is enabled if at least one item is selected', () => {
+ mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) });
wrapper.setData({ selectedItems: [1] });
return wrapper.vm.$nextTick().then(() => {
expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
@@ -183,30 +185,26 @@ describe('Details Page', () => {
describe('on click', () => {
it('when one item is selected', () => {
- wrapper.setData({ selectedItems: [1] });
+ mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) });
+ jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
findBulkDeleteButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findDeleteModal().html()).toContain(
- 'You are about to remove <b>foo</b>. Are you sure?',
- );
- expect(GlModal.methods.show).toHaveBeenCalled();
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'registry_tag_delete',
- });
+ expect(wrapper.vm.itemsToBeDeleted).toEqual([store.state.tags[0]]);
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
});
});
it('when multiple items are selected', () => {
- wrapper.setData({ selectedItems: [0, 1] });
+ mountComponent({
+ data: () => ({ selectedItems: store.state.tags.map(t => t.name) }),
+ });
findBulkDeleteButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findDeleteModal().html()).toContain(
- 'You are about to remove <b>2</b> tags. Are you sure?',
- );
- expect(GlModal.methods.show).toHaveBeenCalled();
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'bulk_registry_tag_delete',
- });
+
+ expect(wrapper.vm.itemsToBeDeleted).toEqual(tagsListResponse.data);
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'bulk_registry_tag_delete',
});
});
});
@@ -237,14 +235,10 @@ describe('Details Page', () => {
findAllDeleteButtons()
.at(0)
.vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findDeleteModal().html()).toContain(
- 'You are about to remove <b>bar</b>. Are you sure?',
- );
- expect(GlModal.methods.show).toHaveBeenCalled();
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'registry_tag_delete',
- });
+
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
});
});
});
@@ -292,6 +286,7 @@ describe('Details Page', () => {
let timeCell;
beforeEach(() => {
+ mountComponent();
timeCell = findFirstRowItem('rowTime');
});
@@ -331,176 +326,97 @@ describe('Details Page', () => {
});
describe('modal', () => {
- beforeEach(() => {
- mountComponent();
- });
-
it('exists', () => {
+ mountComponent();
expect(findDeleteModal().exists()).toBe(true);
});
- describe('when ok event is emitted', () => {
- beforeEach(() => {
- dispatchSpy.mockResolvedValue();
- });
-
- it('tracks confirm_delete', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'confirm_delete', {
- label: 'registry_tag_delete',
- });
+ describe('cancel event', () => {
+ it('tracks cancel_delete', () => {
+ mountComponent();
+ findDeleteModal().vm.$emit('cancel');
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
+ label: 'registry_tag_delete',
});
});
+ });
- describe('when only one element is selected', () => {
- it('execute the delete and remove selection', () => {
- wrapper.setData({ itemsToBeDeleted: [0] });
- findDeleteModal().vm.$emit('ok');
+ describe('confirmDelete event', () => {
+ describe('when one item is selected to be deleted', () => {
+ const itemsToBeDeleted = [{ name: 'foo' }];
- expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
- tag: store.state.tags[0],
- params: wrapper.vm.$route.params.id,
+ it('dispatch requestDeleteTag with the right parameters', () => {
+ mountComponent({ data: () => ({ itemsToBeDeleted }) });
+ findDeleteModal().vm.$emit('confirmDelete');
+ expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', {
+ tag: itemsToBeDeleted[0],
+ params: routeId,
});
- // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
- expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
- expect(wrapper.vm.selectedItems).toEqual([]);
- expect(findCheckedCheckboxes()).toHaveLength(0);
+ });
+ it('remove the deleted item from the selected items', () => {
+ mountComponent({ data: () => ({ itemsToBeDeleted, selectedItems: ['foo', 'bar'] }) });
+ findDeleteModal().vm.$emit('confirmDelete');
+ expect(wrapper.vm.selectedItems).toEqual(['bar']);
});
});
- describe('when multiple elements are selected', () => {
+ describe('when more than one item is selected to be deleted', () => {
beforeEach(() => {
- wrapper.setData({ itemsToBeDeleted: [0, 1] });
+ mountComponent({
+ data: () => ({
+ itemsToBeDeleted: [{ name: 'foo' }, { name: 'bar' }],
+ selectedItems: ['foo', 'bar'],
+ }),
+ });
});
- it('execute the delete and remove selection', () => {
- findDeleteModal().vm.$emit('ok');
-
- expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
- ids: store.state.tags.map(t => t.name),
- params: wrapper.vm.$route.params.id,
+ it('dispatch requestDeleteTags with the right parameters', () => {
+ findDeleteModal().vm.$emit('confirmDelete');
+ expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
+ ids: ['foo', 'bar'],
+ params: routeId,
});
- // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
- expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
- expect(findCheckedCheckboxes()).toHaveLength(0);
+ });
+ it('clears the selectedItems', () => {
+ findDeleteModal().vm.$emit('confirmDelete');
+ expect(wrapper.vm.selectedItems).toEqual([]);
});
});
});
+ });
- it('tracks cancel_delete when cancel event is emitted', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('cancel');
- return wrapper.vm.$nextTick().then(() => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
- label: 'registry_tag_delete',
- });
- });
+ describe('Header', () => {
+ it('exists', () => {
+ mountComponent();
+ expect(findDetailsHeader().exists()).toBe(true);
+ });
+
+ it('has the correct props', () => {
+ mountComponent();
+ expect(findDetailsHeader().props()).toEqual({ imageName: 'foo' });
});
});
- describe('Delete alert', () => {
+ describe('Delete Alert', () => {
const config = {
- garbageCollectionHelpPagePath: 'foo',
+ isAdmin: true,
+ garbageCollectionHelpPagePath: 'baz',
};
+ const deleteAlertType = 'success_tag';
- describe('when the user is an admin', () => {
- beforeEach(() => {
- store.commit(SET_INITIAL_STATE, { ...config, isAdmin: true });
- });
-
- afterEach(() => {
- store.commit(SET_INITIAL_STATE, config);
- });
-
- describe.each`
- deleteType | successTitle | errorTitle
- ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
- ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
- `('behaves correctly on $deleteType', ({ deleteType, successTitle, errorTitle }) => {
- describe('when delete is successful', () => {
- beforeEach(() => {
- dispatchSpy.mockResolvedValue();
- mountComponent();
- return wrapper.vm[deleteType]('foo');
- });
-
- it('alert exists', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('alert body contains admin tip', () => {
- expect(
- findAlert()
- .text()
- .replace(/\s\s+/gm, ' '),
- ).toBe(ADMIN_GARBAGE_COLLECTION_TIP.replace(/%{\w+}/gm, ''));
- });
-
- it('alert body contains link', () => {
- const alertLink = findAlert().find(GlLink);
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.attributes('href')).toBe(config.garbageCollectionHelpPagePath);
- });
-
- it('alert title is appropriate', () => {
- expect(findAlert().attributes('title')).toBe(successTitle);
- });
- });
-
- describe('when delete is not successful', () => {
- beforeEach(() => {
- mountComponent();
- dispatchSpy.mockRejectedValue();
- return wrapper.vm[deleteType]('foo');
- });
+ it('exists', () => {
+ mountComponent();
+ expect(findDeleteAlert().exists()).toBe(true);
+ });
- it('alert exist and text is appropriate', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorTitle);
- });
- });
+ it('has the correct props', () => {
+ store.commit(SET_INITIAL_STATE, { ...config });
+ mountComponent({
+ data: () => ({
+ deleteAlertType,
+ }),
});
+ expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
});
-
- describe.each`
- deleteType | successTitle | errorTitle
- ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
- ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
- `(
- 'when the user is not an admin alert behaves correctly on $deleteType',
- ({ deleteType, successTitle, errorTitle }) => {
- beforeEach(() => {
- store.commit('SET_INITIAL_STATE', { ...config });
- });
-
- describe('when delete is successful', () => {
- beforeEach(() => {
- dispatchSpy.mockResolvedValue();
- mountComponent();
- return wrapper.vm[deleteType]('foo');
- });
-
- it('alert exist and text is appropriate', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(successTitle);
- });
- });
-
- describe('when delete is not successful', () => {
- beforeEach(() => {
- mountComponent();
- dispatchSpy.mockRejectedValue();
- return wrapper.vm[deleteType]('foo');
- });
-
- it('alert exist and text is appropriate', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorTitle);
- });
- });
- },
- );
});
});
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 10e7d7b9cc7..fe21ab71290 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -211,7 +211,8 @@ describe Gitlab::UsageData, :aggregate_failures do
describe '#features_usage_data_ce' do
subject { described_class.features_usage_data_ce }
- it 'gathers feature usage data' do
+ it 'gathers feature usage data', :aggregate_failures do
+ expect(subject[:instance_auto_devops_enabled]).to eq(Gitlab::CurrentSettings.auto_devops_enabled?)
expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled)
expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?)
expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled)
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index fae0c636bdc..c3890c72852 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -3,11 +3,17 @@
require 'spec_helper'
describe ObjectStorage::DirectUpload do
+ let(:region) { 'us-east-1' }
+ let(:path_style) { false }
+ let(:use_iam_profile) { false }
let(:credentials) do
{
provider: 'AWS',
aws_access_key_id: 'AWS_ACCESS_KEY_ID',
- aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY'
+ aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY',
+ region: region,
+ path_style: path_style,
+ use_iam_profile: use_iam_profile
}
end
@@ -57,6 +63,62 @@ describe ObjectStorage::DirectUpload do
describe '#to_hash' do
subject { direct_upload.to_hash }
+ shared_examples 'a valid S3 upload' do
+ it_behaves_like 'a valid upload'
+
+ it 'sets Workhorse client data' do
+ expect(subject[:UseWorkhorseClient]).to eq(use_iam_profile)
+ expect(subject[:RemoteTempObjectID]).to eq(object_name)
+
+ object_store_config = subject[:ObjectStorage]
+ expect(object_store_config[:Provider]).to eq 'AWS'
+
+ s3_config = object_store_config[:S3Config]
+ expect(s3_config[:Bucket]).to eq(bucket_name)
+ expect(s3_config[:Region]).to eq(region)
+ expect(s3_config[:PathStyle]).to eq(path_style)
+ expect(s3_config[:UseIamProfile]).to eq(use_iam_profile)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(use_workhorse_s3_client: false)
+ end
+
+ it 'does not enable Workhorse client' do
+ expect(subject[:UseWorkhorseClient]).to be false
+ end
+ end
+
+ context 'when V2 signatures are used' do
+ before do
+ credentials[:aws_signature_version] = 2
+ end
+
+ it 'does not enable Workhorse client' do
+ expect(subject[:UseWorkhorseClient]).to be false
+ end
+ end
+
+ context 'when V4 signatures are used' do
+ before do
+ credentials[:aws_signature_version] = 4
+ end
+
+ it 'enables the Workhorse client for instance profiles' do
+ expect(subject[:UseWorkhorseClient]).to eq(use_iam_profile)
+ end
+ end
+ end
+
+ shared_examples 'a valid Google upload' do
+ it_behaves_like 'a valid upload'
+
+ it 'does not set Workhorse client data' do
+ expect(subject.keys).not_to include(:UseWorkhorseClient, :RemoteTempObjectID, :ObjectStorage)
+ end
+ end
+
shared_examples 'a valid upload' do
it "returns valid structure" do
expect(subject).to have_key(:Timeout)
@@ -97,6 +159,16 @@ describe ObjectStorage::DirectUpload do
end
end
+ shared_examples 'a valid S3 upload without multipart data' do
+ it_behaves_like 'a valid S3 upload'
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ shared_examples 'a valid S3 upload with multipart data' do
+ it_behaves_like 'a valid S3 upload'
+ it_behaves_like 'a valid upload with multipart data'
+ end
+
shared_examples 'a valid upload without multipart data' do
it_behaves_like 'a valid upload'
@@ -109,13 +181,50 @@ describe ObjectStorage::DirectUpload do
context 'when length is known' do
let(:has_length) { true }
- it_behaves_like 'a valid upload without multipart data'
+ it_behaves_like 'a valid S3 upload without multipart data'
+
+ context 'when path style is true' do
+ let(:path_style) { true }
+ let(:storage_url) { 'https://s3.amazonaws.com/uploads' }
+
+ before do
+ stub_object_storage_multipart_init(storage_url, "myUpload")
+ end
+
+ it_behaves_like 'a valid S3 upload without multipart data'
+ end
+
+ context 'when IAM profile is true' do
+ let(:use_iam_profile) { true }
+ let(:iam_credentials_url) { "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
+ let(:iam_credentials) do
+ {
+ 'AccessKeyId' => 'dummykey',
+ 'SecretAccessKey' => 'dummysecret',
+ 'Token' => 'dummytoken',
+ 'Expiration' => 1.day.from_now.xmlschema
+ }
+ end
+
+ before do
+ stub_request(:get, iam_credentials_url)
+ .to_return(status: 200, body: "somerole", headers: {})
+ stub_request(:get, "#{iam_credentials_url}somerole")
+ .to_return(status: 200, body: iam_credentials.to_json, headers: {})
+ end
+
+ it_behaves_like 'a valid S3 upload without multipart data'
+ end
end
context 'when length is unknown' do
let(:has_length) { false }
- it_behaves_like 'a valid upload with multipart data' do
+ it_behaves_like 'a valid S3 upload with multipart data' do
+ before do
+ stub_object_storage_multipart_init(storage_url, "myUpload")
+ end
+
context 'when maximum upload size is 10MB' do
let(:maximum_size) { 10.megabyte }
@@ -169,12 +278,14 @@ describe ObjectStorage::DirectUpload do
context 'when length is known' do
let(:has_length) { true }
+ it_behaves_like 'a valid Google upload'
it_behaves_like 'a valid upload without multipart data'
end
context 'when length is unknown' do
let(:has_length) { false }
+ it_behaves_like 'a valid Google upload'
it_behaves_like 'a valid upload without multipart data'
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 4ef6496f78e..14066b1e9d2 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -68,20 +68,13 @@ describe Event do
end
end
- describe 'after_create :track_user_interacted_projects' do
+ describe 'after_create UserInteractedProject.track' do
let(:event) { build(:push_event, project: project, author: project.owner) }
it 'passes event to UserInteractedProject.track' do
- expect(UserInteractedProject).to receive(:available?).and_return(true)
expect(UserInteractedProject).to receive(:track).with(event)
event.save
end
-
- it 'does not call UserInteractedProject.track if its not yet available' do
- expect(UserInteractedProject).to receive(:available?).and_return(false)
- expect(UserInteractedProject).not_to receive(:track)
- event.save
- end
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 33d03bfc0f5..0dfb59cf43a 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -88,33 +88,6 @@ describe InternalId do
expect(normalized).to eq((0..seq.size - 1).to_a)
end
-
- context 'with an insufficient schema version' do
- before do
- described_class.reset_column_information
- # Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
- end
-
- let(:init) { double('block') }
-
- it 'calculates next internal ids on the fly' do
- val = rand(1..100)
-
- expect(init).to receive(:call).with(issue).and_return(val)
- expect(subject).to eq(val + 1)
- end
-
- it 'always attempts to generate internal IDs in production mode' do
- stub_rails_env('production')
-
- val = rand(1..100)
- generator = double(generate: val)
- expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator)
-
- expect(subject).to eq(val)
- end
- end
end
describe '.reset' do
@@ -152,20 +125,6 @@ describe InternalId do
described_class.generate_next(issue, scope, usage, init)
end
end
-
- context 'with an insufficient schema version' do
- let(:value) { 2 }
-
- before do
- described_class.reset_column_information
- # Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
- end
-
- it 'does not reset any of the iids' do
- expect(subject).to be_falsey
- end
- end
end
describe '.track_greatest' do
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 86115a61aa7..ecca371ce4e 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -3,25 +3,6 @@
require 'spec_helper'
describe ProjectCiCdSetting do
- describe '.available?' do
- before do
- described_class.reset_column_information
- end
-
- it 'returns true' do
- expect(described_class).to be_available
- end
-
- it 'memoizes the schema version' do
- expect(ActiveRecord::Migrator)
- .to receive(:current_version)
- .and_call_original
- .once
-
- 2.times { described_class.available? }
- end
- end
-
describe 'validations' do
it 'validates default_git_depth is between 0 and 1000 or nil' do
expect(subject).to validate_numericality_of(:default_git_depth)
diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb
index 75386e220f5..83c66bf1969 100644
--- a/spec/models/user_interacted_project_spec.rb
+++ b/spec/models/user_interacted_project_spec.rb
@@ -44,21 +44,6 @@ describe UserInteractedProject do
end
end
- describe '.available?' do
- before do
- described_class.instance_variable_set('@available_flag', nil)
- end
-
- it 'checks schema version and properly caches positive result' do
- expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION - 1 - rand(1000))
- expect(described_class.available?).to be_falsey
- expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION + rand(1000))
- expect(described_class.available?).to be_truthy
- expect(ActiveRecord::Migrator).not_to receive(:current_version)
- expect(described_class.available?).to be_truthy # cached response
- end
- end
-
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:user_id) }
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 053b5c3a793..6ec63ba61ca 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -833,4 +833,63 @@ describe ProjectPolicy do
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
end
+
+ describe 'read_repository_graphs' do
+ subject { described_class.new(guest, project) }
+
+ before do
+ allow(subject).to receive(:allowed?).with(:read_repository_graphs).and_call_original
+ allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code)
+ end
+
+ context 'when user can download_code' do
+ let(:can_download_code) { true }
+
+ it { is_expected.to be_allowed(:read_repository_graphs) }
+ end
+
+ context 'when user cannot download_code' do
+ let(:can_download_code) { false }
+
+ it { is_expected.to be_disallowed(:read_repository_graphs) }
+ end
+ end
+
+ describe 'read_build_report_results' do
+ subject { described_class.new(guest, project) }
+
+ before do
+ allow(subject).to receive(:allowed?).with(:read_build_report_results).and_call_original
+ allow(subject).to receive(:allowed?).with(:read_build).and_return(can_read_build)
+ allow(subject).to receive(:allowed?).with(:read_pipeline).and_return(can_read_pipeline)
+ end
+
+ context 'when user can read_build and read_pipeline' do
+ let(:can_read_build) { true }
+ let(:can_read_pipeline) { true }
+
+ it { is_expected.to be_allowed(:read_build_report_results) }
+ end
+
+ context 'when user can read_build but cannot read_pipeline' do
+ let(:can_read_build) { true }
+ let(:can_read_pipeline) { false }
+
+ it { is_expected.to be_disallowed(:read_build_report_results) }
+ end
+
+ context 'when user cannot read_build but can read_pipeline' do
+ let(:can_read_build) { false }
+ let(:can_read_pipeline) { true }
+
+ it { is_expected.to be_disallowed(:read_build_report_results) }
+ end
+
+ context 'when user cannot read_build and cannot read_pipeline' do
+ let(:can_read_build) { false }
+ let(:can_read_pipeline) { false }
+
+ it { is_expected.to be_disallowed(:read_build_report_results) }
+ end
+ end
end
diff --git a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
new file mode 100644
index 00000000000..937c709218f
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/bulk_insert'
+
+describe RuboCop::Cop::Gitlab::BulkInsert do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags the use of Gitlab::Database.bulk_insert' do
+ expect_offense(<<~SOURCE)
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, instead of using `Gitlab::Database.bulk_insert`. See https://docs.gitlab.com/ee/development/insert_into_tables_in_batches.html
+ SOURCE
+ end
+end
diff --git a/spec/services/clusters/applications/prometheus_config_service_spec.rb b/spec/services/clusters/applications/prometheus_config_service_spec.rb
index 993a697b543..b9032e665ec 100644
--- a/spec/services/clusters/applications/prometheus_config_service_spec.rb
+++ b/spec/services/clusters/applications/prometheus_config_service_spec.rb
@@ -90,23 +90,25 @@ describe Clusters::Applications::PrometheusConfigService do
create(:prometheus_alert,
project: project,
environment: production,
- prometheus_metric: metric)
+ prometheus_metric: metric,
+ operator: PrometheusAlert.operators['gt'],
+ threshold: 0)
end
let(:metric) do
create(:prometheus_metric, query: query, project: project)
end
- let(:query) { '%{ci_environment_slug}' }
+ let(:query) { 'up{environment="{{ci_environment_slug}}"}' }
it 'substitutes query variables' do
expect(Gitlab::Prometheus::QueryVariables)
.to receive(:call)
- .with(production)
+ .with(production, start_time: nil, end_time: nil)
.and_call_original
expr = groups.dig(0, 'rules', 0, 'expr')
- expect(expr).to include(production.name)
+ expect(expr).to eq("up{environment=\"#{production.slug}\"} > 0.0")
end
end
@@ -127,13 +129,15 @@ describe Clusters::Applications::PrometheusConfigService do
end
it 'substitutes query variables once per environment' do
+ allow(Gitlab::Prometheus::QueryVariables).to receive(:call).and_call_original
+
expect(Gitlab::Prometheus::QueryVariables)
.to receive(:call)
- .with(production)
+ .with(production, start_time: nil, end_time: nil)
expect(Gitlab::Prometheus::QueryVariables)
.to receive(:call)
- .with(staging)
+ .with(staging, start_time: nil, end_time: nil)
subject
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index f82a3cee1d9..c791c454d70 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -42,13 +42,11 @@ describe Issuable::BulkUpdateService do
let(:issue_no_labels) { create(:issue, project: project) }
let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] }
- let(:labels) { [] }
let(:add_labels) { [] }
let(:remove_labels) { [] }
let(:bulk_update_params) do
{
- label_ids: labels.map(&:id),
add_label_ids: add_labels.map(&:id),
remove_label_ids: remove_labels.map(&:id)
}
@@ -58,27 +56,6 @@ describe Issuable::BulkUpdateService do
bulk_update(issues, bulk_update_params)
end
- context 'when label_ids are passed' do
- let(:issues) { [issue_all_labels, issue_no_labels] }
- let(:labels) { [bug, regression] }
-
- it 'updates the labels of all issues passed to the labels passed' do
- expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id)))
- end
-
- it 'does not update issues not passed in' do
- expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
- end
-
- context 'when those label IDs are empty' do
- let(:labels) { [] }
-
- it 'updates the issues passed to have no labels' do
- expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
- end
- end
- end
-
context 'when add_label_ids are passed' do
let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
let(:add_labels) { [bug, regression, merge_requests] }
@@ -122,69 +99,21 @@ describe Issuable::BulkUpdateService do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
end
+ end
- context 'when add_label_ids and label_ids are passed' do
- let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] }
- let(:labels) { [merge_requests] }
- let(:add_labels) { [regression] }
-
- it 'adds the label IDs to all issues passed' do
- expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id))
- end
-
- it 'ignores the label IDs parameter' do
- expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
- end
-
- it 'does not update issues not passed in' do
- expect(issue_no_labels.label_ids).to be_empty
- end
- end
-
- context 'when remove_label_ids and label_ids are passed' do
- let(:issues) { [issue_no_labels, issue_bug_and_regression] }
- let(:labels) { [merge_requests] }
- let(:remove_labels) { [regression] }
-
- it 'removes the label IDs from all issues passed' do
- expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(regression.id)
- end
-
- it 'ignores the label IDs parameter' do
- expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(merge_requests.id)
- end
-
- it 'does not update issues not passed in' do
- expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id)
- end
- end
-
- context 'when add_label_ids, remove_label_ids, and label_ids are passed' do
- let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] }
- let(:labels) { [regression] }
- let(:add_labels) { [bug] }
- let(:remove_labels) { [merge_requests] }
-
- it 'adds the label IDs to all issues passed' do
- expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
- end
+ context 'with issuables at a project level' do
+ let(:parent) { project }
- it 'removes the label IDs from all issues passed' do
- expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(merge_requests.id)
- end
+ context 'with unpermitted attributes' do
+ let(:issues) { create_list(:issue, 2, project: project) }
+ let(:label) { create(:label, project: project) }
- it 'ignores the label IDs parameter' do
- expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(regression.id)
- end
+ it 'does not update the issues' do
+ bulk_update(issues, label_ids: [label.id])
- it 'does not update issues not passed in' do
- expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ expect(issues.map(&:reload).map(&:label_ids)).not_to include(label.id)
end
end
- end
-
- context 'with issuables at a project level' do
- let(:parent) { project }
describe 'close issues' do
let(:issues) { create_list(:issue, 2, project: project) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 80039049bc3..33ae2682d01 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
describe Issues::UpdateService, :mailer do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :repository, group: group) }
- let(:label) { create(:label, project: project) }
- let(:label2) { create(:label) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
+ let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:label2) { create(:label, project: project) }
let(:issue) do
create(:issue, title: 'Old title',
@@ -19,7 +19,7 @@ describe Issues::UpdateService, :mailer do
author: create(:user))
end
- before do
+ before_all do
project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
@@ -669,28 +669,24 @@ describe Issues::UpdateService, :mailer do
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
- it 'ignores the label_ids parameter' do
- expect(result.label_ids).not_to include(label.id)
+ before do
+ issue.update(labels: [label2])
end
- it 'adds the passed labels' do
- expect(result.label_ids).to include(label3.id)
+ it 'replaces the labels with the ones in label_ids and adds those in add_label_ids' do
+ expect(result.label_ids).to contain_exactly(label.id, label3.id)
end
end
context 'when remove_label_ids and label_ids are passed' do
- let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
+ let(:params) { { label_ids: [label.id, label2.id, label3.id], remove_label_ids: [label.id] } }
before do
issue.update(labels: [label, label3])
end
- it 'ignores the label_ids parameter' do
- expect(result.label_ids).not_to be_empty
- end
-
- it 'removes the passed labels' do
- expect(result.label_ids).not_to include(label.id)
+ it 'replaces the labels with the ones in label_ids and removes those in remove_label_ids' do
+ expect(result.label_ids).to contain_exactly(label2.id, label3.id)
end
end
diff --git a/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb b/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb
new file mode 100644
index 00000000000..440edd376e0
--- /dev/null
+++ b/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/issues/import_csv/_button' do
+ include Devise::Test::ControllerHelpers
+
+ context 'when the user does not have edit permissions' do
+ before do
+ render
+ end
+
+ it 'shows a dropdown button to import CSV' do
+ expect(rendered).to have_text('Import CSV')
+ end
+
+ it 'does not show a button to import from Jira' do
+ expect(rendered).not_to have_text('Import from Jira')
+ end
+ end
+
+ context 'when the user has edit permissions' do
+ let(:project) { create(:project) }
+ let(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ before do
+ allow(view).to receive(:project_import_jira_path).and_return('import/jira')
+ allow(view).to receive(:current_user).and_return(current_user)
+
+ assign(:project, project)
+
+ render
+ end
+
+ it 'shows a dropdown button to import CSV' do
+ expect(rendered).to have_text('Import CSV')
+ end
+
+ it 'shows a button to import from Jira' do
+ expect(rendered).to have_text('Import from Jira')
+ end
+ end
+end
diff --git a/vendor/project_templates/learn_gitlab.tar.gz b/vendor/project_templates/learn_gitlab.tar.gz
new file mode 100644
index 00000000000..1199b6dac02
--- /dev/null
+++ b/vendor/project_templates/learn_gitlab.tar.gz
Binary files differ