summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue21
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue21
-rw-r--r--app/assets/javascripts/alert_management/constants.js27
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js10
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue29
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue21
-rw-r--r--app/assets/javascripts/error_tracking/utils.js27
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue4
-rw-r--r--app/assets/stylesheets/pages/storage_quota.scss23
-rw-r--r--app/assets/stylesheets/utilities.scss16
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/finders/resource_milestone_event_finder.rb69
-rw-r--r--app/models/ci/instance_variable.rb5
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/event.rb4
-rw-r--r--app/models/resource_milestone_event.rb10
-rw-r--r--app/services/groups/import_export/export_service.rb6
-rw-r--r--app/services/groups/import_export/import_service.rb5
-rw-r--r--changelogs/unreleased/210550-conan-export-tgz.yml5
-rw-r--r--changelogs/unreleased/217680-health-metrics-instrumentation.yml5
-rw-r--r--changelogs/unreleased/217743-match-commits-filter-author-button-to-spec.yml5
-rw-r--r--changelogs/unreleased/217936-validate-the-size-of-the-value-for-instance-level-variables.yml5
-rw-r--r--changelogs/unreleased/218757-fix-polling-for-events.yml5
-rw-r--r--changelogs/unreleased/Remove-removeAllAssignees-logic-from-issue-model.yml5
-rw-r--r--changelogs/unreleased/Remove-removeAssignee-logic-from-issue-model.yml5
-rw-r--r--changelogs/unreleased/ab-services-partial-indexes.yml5
-rw-r--r--changelogs/unreleased/add-api-endpoint-for-resource-milestone-events-pd.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-import-export-tmp-folder-cleanup.yml5
-rw-r--r--db/migrate/20200526120714_change_partial_indexes_on_services.rb26
-rw-r--r--db/structure.sql5
-rw-r--r--doc/api/instance_level_ci_variables.md2
-rw-r--r--doc/api/resource_milestone_events.md224
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities/resource_milestone_event.rb20
-rw-r--r--lib/api/resource_milestone_events.rb54
-rw-r--r--lib/gitlab/import_export/importer.rb5
-rw-r--r--lib/gitlab/import_export/saver.rb13
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb6
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb44
-rw-r--r--spec/finders/resource_milestone_event_finder_spec.rb83
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js34
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js44
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js71
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js39
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb12
-rw-r--r--spec/models/ci/instance_variable_spec.rb1
-rw-r--r--spec/models/ci/pipeline_spec.rb14
-rw-r--r--spec/models/resource_milestone_event_spec.rb30
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb16
-rw-r--r--spec/requests/api/resource_milestone_events_spec.rb27
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb4
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb4
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb22
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb66
59 files changed, 1124 insertions, 139 deletions
diff --git a/.gitignore b/.gitignore
index 3120c1c1bdc..a2441d61bc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,3 +92,4 @@ jsdoc/
webpack-dev-server.json
/.nvimrc
.solargraph.yml
+apollo.config.js
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index bb9f092a9ae..c08b4fb2f63 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -18,10 +18,15 @@ import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { ALERTS_SEVERITY_LABELS } from '../constants';
+import {
+ ALERTS_SEVERITY_LABELS,
+ trackAlertsDetailsViewsOptions,
+ trackAlertStatusUpdateOptions,
+} from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
export default {
statuses: {
@@ -108,6 +113,9 @@ export default {
return this.errored && !this.isErrorDismissed;
},
},
+ mounted() {
+ this.trackPageViews();
+ },
methods: {
dismissError() {
this.isErrorDismissed = true;
@@ -122,6 +130,9 @@ export default {
projectPath: this.projectPath,
},
})
+ .then(() => {
+ this.trackStatusUpdate(status);
+ })
.catch(() => {
createFlash(
s__(
@@ -157,6 +168,14 @@ export default {
issuePath(issueId) {
return joinPaths(this.projectIssuesPath, issueId);
},
+ trackPageViews() {
+ const { category, action } = trackAlertsDetailsViewsOptions;
+ Tracking.event(category, action);
+ },
+ trackStatusUpdate(status) {
+ const { category, action, label } = trackAlertStatusUpdateOptions;
+ Tracking.event(category, action, { label, property: status });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index a1a71e592f6..74ce76739a2 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -19,13 +19,20 @@ import { fetchPolicies } from '~/lib/graphql';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
-import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
+import {
+ ALERTS_STATUS,
+ ALERTS_STATUS_TABS,
+ ALERTS_SEVERITY_LABELS,
+ trackAlertListViewsOptions,
+ trackAlertStatusUpdateOptions,
+} from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import { capitalizeFirstCharacter, convertToSnakeCase } from '~/lib/utils/text_utility';
+import Tracking from '~/tracking';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
const bodyTrClass =
- 'gl-border-1 gl-border-t-solid gl-border-gray-100 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200';
+ 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
const findDefaultSortColumn = () => document.querySelector('.js-started-at');
export default {
@@ -182,6 +189,7 @@ export default {
},
mounted() {
findDefaultSortColumn().ariaSort = 'ascending';
+ this.trackPageViews();
},
methods: {
filterAlertsByStatus(tabIndex) {
@@ -208,6 +216,7 @@ export default {
},
})
.then(() => {
+ this.trackStatusUpdate(status);
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
})
@@ -222,6 +231,14 @@ export default {
navigateToAlertDetails({ iid }) {
return visitUrl(joinPaths(window.location.pathname, iid, 'details'));
},
+ trackPageViews() {
+ const { category, action } = trackAlertListViewsOptions;
+ Tracking.event(category, action);
+ },
+ trackStatusUpdate(status) {
+ const { category, action, label } = trackAlertStatusUpdateOptions;
+ Tracking.event(category, action, { label, property: status });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index 9df01d9d0b5..a8f5d6cfe30 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -44,3 +44,30 @@ export const ALERTS_STATUS_TABS = [
filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED, ALERTS_STATUS.RESOLVED],
},
];
+
+/* eslint-disable @gitlab/require-i18n-strings */
+
+/**
+ * Tracks snowplow event when user views alerts list
+ */
+export const trackAlertListViewsOptions = {
+ category: 'Alert Management',
+ action: 'view_alerts_list',
+};
+
+/**
+ * Tracks snowplow event when user views alert details
+ */
+export const trackAlertsDetailsViewsOptions = {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+};
+
+/**
+ * Tracks snowplow event when alert status is updated
+ */
+export const trackAlertStatusUpdateOptions = {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+};
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 878f49cc6be..02fd0334403 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -60,13 +60,11 @@ class ListIssue {
}
removeAssignee(removeAssignee) {
- if (removeAssignee) {
- this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
- }
+ boardsStore.removeIssueAssignee(this, removeAssignee);
}
removeAllAssignees() {
- this.assignees = [];
+ boardsStore.removeAllIssueAssignees(this);
}
addMilestone(milestone) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index fdbd7e89bfb..feef405d52e 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -682,10 +682,20 @@ const boardsStore = {
...this.multiSelect.list.slice(index + 1),
];
},
+ removeIssueAssignee(issue, removeAssignee) {
+ if (removeAssignee) {
+ issue.assignees = issue.assignees.filter(assignee => assignee.id !== removeAssignee.id);
+ }
+ },
clearMultiSelect() {
this.multiSelect.list = [];
},
+
+ removeAllIssueAssignees(issue) {
+ issue.assignees = [];
+ },
+
refreshIssueData(issue, obj) {
issue.id = obj.id;
issue.iid = obj.iid;
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 148edfe3a51..da079877c72 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -20,8 +20,13 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { trackClickErrorLinkToSentryOptions } from '../utils';
import { severityLevel, severityLevelVariant, errorStatus } from './constants';
+import Tracking from '~/tracking';
+import {
+ trackClickErrorLinkToSentryOptions,
+ trackErrorDetailsViewsOptions,
+ trackErrorStatusUpdateOptions,
+} from '../utils';
import query from '../queries/details.query.graphql';
@@ -172,6 +177,7 @@ export default {
},
},
mounted() {
+ this.trackPageViews();
this.startPollingStacktrace(this.issueStackTracePath);
this.errorPollTimeout = Date.now() + SENTRY_TIMEOUT;
this.$apollo.queries.error.setOptions({
@@ -194,7 +200,10 @@ export default {
onIgnoreStatusUpdate() {
const status =
this.errorStatus === errorStatus.IGNORED ? errorStatus.UNRESOLVED : errorStatus.IGNORED;
- this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status });
+ // eslint-disable-next-line promise/catch-or-return
+ this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status }).then(() => {
+ this.trackStatusUpdate(status);
+ });
},
onResolveStatusUpdate() {
const status =
@@ -206,6 +215,7 @@ export default {
if (this.closedIssueId) {
this.isAlertVisible = true;
}
+ this.trackStatusUpdate(status);
});
},
onNoApolloResult() {
@@ -218,6 +228,14 @@ export default {
formatDate(date) {
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
+ trackPageViews() {
+ const { category, action } = trackErrorDetailsViewsOptions;
+ Tracking.event(category, action);
+ },
+ trackStatusUpdate(status) {
+ const { category, action, label } = trackErrorStatusUpdateOptions;
+ Tracking.event(category, action, { label, property: status });
+ },
},
};
</script>
@@ -259,7 +277,7 @@ export default {
<div class="d-inline-flex bv-d-sm-down-none">
<gl-deprecated-button
:loading="updatingIgnoreStatus"
- data-qa-selector="update_ignore_status_button"
+ data-testid="update-ignore-status-btn"
@click="onIgnoreStatusUpdate"
>
{{ ignoreBtnLabel }}
@@ -267,7 +285,7 @@ export default {
<gl-deprecated-button
class="btn-outline-info ml-2"
:loading="updatingResolveStatus"
- data-qa-selector="update_resolve_status_button"
+ data-testid="update-resolve-status-btn"
@click="onResolveStatusUpdate"
>
{{ resolveBtnLabel }}
@@ -275,7 +293,7 @@ export default {
<gl-deprecated-button
v-if="error.gitlabIssuePath"
class="ml-2"
- data-qa-selector="view_issue_button"
+ data-testid="view_issue_button"
:href="error.gitlabIssuePath"
variant="success"
>
@@ -375,6 +393,7 @@ export default {
v-track-event="trackClickErrorLinkToSentryOptions(error.externalUrl)"
:href="error.externalUrl"
target="_blank"
+ data-testid="external-url-link"
>
<span class="text-truncate">{{ error.externalUrl }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 45432e8ebd8..111b5ad60a5 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -19,6 +19,8 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import { isEmpty } from 'lodash';
import ErrorTrackingActions from './error_tracking_actions.vue';
+import Tracking from '~/tracking';
+import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
@@ -150,6 +152,9 @@ export default {
this.startPolling();
}
},
+ mounted() {
+ this.trackPageViews();
+ },
methods: {
...mapActions('list', [
'startPolling',
@@ -197,13 +202,25 @@ export default {
this.filterValue = label;
return this.filterByStatus(status);
},
- updateIssueStatus({ errorId, status }) {
+ updateErrosStatus({ errorId, status }) {
+ // eslint-disable-next-line promise/catch-or-return
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
status,
+ }).then(() => {
+ this.trackStatusUpdate(status);
});
+
this.removeIgnoredResolvedErrors(errorId);
},
+ trackPageViews() {
+ const { category, action } = trackErrorListViewsOptions;
+ Tracking.event(category, action);
+ },
+ trackStatusUpdate(status) {
+ const { category, action, label } = trackErrorStatusUpdateOptions;
+ Tracking.event(category, action, { label, property: status });
+ },
},
};
</script>
@@ -359,7 +376,7 @@ export default {
</div>
</template>
<template #cell(status)="errors">
- <error-tracking-actions :error="errors.item" @update-issue-status="updateIssueStatus" />
+ <error-tracking-actions :error="errors.item" @update-issue-status="updateErrosStatus" />
</template>
<template #empty>
{{ __('No errors to display.') }}
diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js
index d1cd70a72fa..e519b8ebfe5 100644
--- a/app/assets/javascripts/error_tracking/utils.js
+++ b/app/assets/javascripts/error_tracking/utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable @gitlab/require-i18n-strings, import/prefer-default-export */
+/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when User clicks on error link to Sentry
@@ -10,3 +10,28 @@ export const trackClickErrorLinkToSentryOptions = url => ({
label: 'Error Link',
property: url,
});
+
+/**
+ * Tracks snowplow event when user views error list
+ */
+export const trackErrorListViewsOptions = {
+ category: 'Error Tracking',
+ action: 'view_errors_list',
+};
+
+/**
+ * Tracks snowplow event when user views error details
+ */
+export const trackErrorDetailsViewsOptions = {
+ category: 'Error Tracking',
+ action: 'view_error_details',
+};
+
+/**
+ * Tracks snowplow event when error status is updated
+ */
+export const trackErrorStatusUpdateOptions = {
+ category: 'Error Tracking',
+ action: 'update_error_status',
+ label: 'Status',
+};
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index eb514b5c070..a8589b50899 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -110,8 +110,8 @@ export default {
<gl-new-dropdown
:text="dropdownText"
:disabled="hasSearchParam"
- toggle-class="gl-py-3"
- class="gl-dropdown w-100 mt-2 mt-sm-0"
+ toggle-class="gl-py-3 gl-border-0"
+ class="w-100 mt-2 mt-sm-0"
>
<gl-new-dropdown-header>
{{ __('Search by author') }}
diff --git a/app/assets/stylesheets/pages/storage_quota.scss b/app/assets/stylesheets/pages/storage_quota.scss
new file mode 100644
index 00000000000..97ae4f0ade4
--- /dev/null
+++ b/app/assets/stylesheets/pages/storage_quota.scss
@@ -0,0 +1,23 @@
+.storage-type-usage {
+ &:first-child {
+ @include gl-rounded-top-left-base;
+ @include gl-rounded-bottom-left-base;
+ }
+
+ &:last-child {
+ @include gl-rounded-top-right-base;
+ @include gl-rounded-bottom-right-base;
+ }
+
+ &:not(:first-child) {
+ @include gl-border-l-1;
+ @include gl-border-l-solid;
+ @include gl-border-white;
+ }
+
+ &:not(:last-child) {
+ @include gl-border-r-1;
+ @include gl-border-r-solid;
+ @include gl-border-white;
+ }
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 2b51bdd825f..803eac52317 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -87,21 +87,5 @@
}
}
-.gl-shim-h-2 {
- height: px-to-rem(4px);
-}
-
-.gl-shim-w-5 {
- width: px-to-rem(16px);
-}
-
-.gl-shim-pb-3 {
- padding-bottom: 8px;
-}
-
-.gl-shim-pt-5 {
- padding-top: 16px;
-}
-
.gl-text-purple { color: $purple; }
.gl-bg-purple-light { background-color: $purple-light; }
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index d4b0d3b2674..d3dfb1813e4 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -13,9 +13,7 @@ module NotesActions
end
def index
- current_fetched_at = Time.current.to_i
-
- notes_json = { notes: [], last_fetched_at: current_fetched_at }
+ notes_json = { notes: [], last_fetched_at: Time.current.to_i }
notes = notes_finder
.execute
@@ -24,7 +22,7 @@ module NotesActions
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
notes =
ResourceEvents::MergeIntoNotesService
- .new(noteable, current_user, last_fetched_at: current_fetched_at)
+ .new(noteable, current_user, last_fetched_at: last_fetched_at)
.execute(notes)
end
diff --git a/app/finders/resource_milestone_event_finder.rb b/app/finders/resource_milestone_event_finder.rb
new file mode 100644
index 00000000000..7af34f0a4bc
--- /dev/null
+++ b/app/finders/resource_milestone_event_finder.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class ResourceMilestoneEventFinder
+ include FinderMethods
+
+ MAX_PER_PAGE = 100
+
+ attr_reader :params, :current_user, :eventable
+
+ def initialize(current_user, eventable, params = {})
+ @current_user = current_user
+ @eventable = eventable
+ @params = params
+ end
+
+ def execute
+ Kaminari.paginate_array(visible_events)
+ end
+
+ private
+
+ def visible_events
+ @visible_events ||= visible_to_user(events)
+ end
+
+ def events
+ @events ||= eventable.resource_milestone_events.include_relations.page(page).per(per_page)
+ end
+
+ def visible_to_user(events)
+ events.select { |event| visible_for_user?(event) }
+ end
+
+ def visible_for_user?(event)
+ milestone = event_milestones[event.milestone_id]
+ return if milestone.blank?
+
+ parent = milestone.parent
+ parent_availabilities[key_for_parent(parent)]
+ end
+
+ def parent_availabilities
+ @parent_availabilities ||= relevant_parents.to_h do |parent|
+ [key_for_parent(parent), Ability.allowed?(current_user, :read_milestone, parent)]
+ end
+ end
+
+ def key_for_parent(parent)
+ "#{parent.class.name}_#{parent.id}"
+ end
+
+ def event_milestones
+ @milestones ||= events.map(&:milestone).uniq.to_h do |milestone|
+ [milestone.id, milestone]
+ end
+ end
+
+ def relevant_parents
+ @relevant_parents ||= event_milestones.map { |_id, milestone| milestone.parent }
+ end
+
+ def per_page
+ [params[:per_page], MAX_PER_PAGE].compact.min
+ end
+
+ def page
+ params[:page] || 1
+ end
+end
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 557f3a63280..89ace3f7ede 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -13,6 +13,11 @@ module Ci
message: "(%{value}) has already been taken"
}
+ validates :encrypted_value, length: {
+ maximum: 1024,
+ too_long: 'The encrypted value of the provided variable exceeds %{count} bytes. Variables over 700 characters risk exceeding the limit.'
+ }
+
scope :unprotected, -> { where(protected: false) }
after_commit { self.class.invalidate_memory_cache(:ci_instance_variable_data) }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2b072b22454..f37525e56e4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -401,7 +401,7 @@ module Ci
# The `Ci::Stage` contains all up-to date data
# as atomic processing updates all data in-bulk
stages
- elsif Feature.enabled?(:ci_pipeline_persisted_stages, default_enabled: true) && complete?
+ elsif complete?
# The `Ci::Stage` contains up-to date data only for `completed` pipelines
# this is due to asynchronous processing of pipeline, and stages possibly
# not updated inline with processing of pipeline
diff --git a/app/models/event.rb b/app/models/event.rb
index 12b85697690..25d016291a7 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -21,6 +21,7 @@ class Event < ApplicationRecord
LEFT = 9 # User left project
DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
+ APPROVED = 12
ACTIONS = HashWithIndifferentAccess.new(
created: CREATED,
@@ -33,7 +34,8 @@ class Event < ApplicationRecord
joined: JOINED,
left: LEFT,
destroyed: DESTROYED,
- expired: EXPIRED
+ expired: EXPIRED,
+ approved: APPROVED
).freeze
WIKI_ACTIONS = [CREATED, UPDATED, DESTROYED].freeze
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index 039f26d8e3f..36068cf508b 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -9,6 +9,8 @@ class ResourceMilestoneEvent < ResourceEvent
validate :exactly_one_issuable
+ scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
+
enum action: {
add: 1,
remove: 2
@@ -26,4 +28,12 @@ class ResourceMilestoneEvent < ResourceEvent
def milestone_title
milestone&.title
end
+
+ def milestone_parent
+ milestone&.parent
+ end
+
+ def issuable
+ issue || merge_request
+ end
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index 39a6889fc84..abac0ffc5d9 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -22,7 +22,7 @@ module Groups
save!
ensure
- cleanup
+ remove_base_tmp_dir
end
private
@@ -81,8 +81,8 @@ module Groups
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end
- def cleanup
- FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
+ def remove_base_tmp_dir
+ FileUtils.rm_rf(shared.base_path) if shared&.base_path
end
def notify_error!
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index dcd78210801..bd611d55847 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -26,6 +26,7 @@ module Groups
end
ensure
+ remove_base_tmp_dir
remove_import_file
end
@@ -102,6 +103,10 @@ module Groups
raise Gitlab::ImportExport::Error.new(@shared.errors.to_sentence)
end
+
+ def remove_base_tmp_dir
+ FileUtils.rm_rf(@shared.base_path)
+ end
end
end
end
diff --git a/changelogs/unreleased/210550-conan-export-tgz.yml b/changelogs/unreleased/210550-conan-export-tgz.yml
new file mode 100644
index 00000000000..bffd0b8b644
--- /dev/null
+++ b/changelogs/unreleased/210550-conan-export-tgz.yml
@@ -0,0 +1,5 @@
+---
+title: Conan package registry support for the conan_export.tgz file
+merge_request: 32866
+author:
+type: fixed
diff --git a/changelogs/unreleased/217680-health-metrics-instrumentation.yml b/changelogs/unreleased/217680-health-metrics-instrumentation.yml
new file mode 100644
index 00000000000..77ae44aaf38
--- /dev/null
+++ b/changelogs/unreleased/217680-health-metrics-instrumentation.yml
@@ -0,0 +1,5 @@
+---
+title: Monitor:Health metrics instrumenation
+merge_request: 32846
+author:
+type: added
diff --git a/changelogs/unreleased/217743-match-commits-filter-author-button-to-spec.yml b/changelogs/unreleased/217743-match-commits-filter-author-button-to-spec.yml
new file mode 100644
index 00000000000..c90ceb3190b
--- /dev/null
+++ b/changelogs/unreleased/217743-match-commits-filter-author-button-to-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Make commits author button confirm to Pajamas specs
+merge_request: 32821
+author:
+type: fixed
diff --git a/changelogs/unreleased/217936-validate-the-size-of-the-value-for-instance-level-variables.yml b/changelogs/unreleased/217936-validate-the-size-of-the-value-for-instance-level-variables.yml
new file mode 100644
index 00000000000..b62aa0a8c20
--- /dev/null
+++ b/changelogs/unreleased/217936-validate-the-size-of-the-value-for-instance-level-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add value length validations for instance level variable
+merge_request: 32303
+author:
+type: fixed
diff --git a/changelogs/unreleased/218757-fix-polling-for-events.yml b/changelogs/unreleased/218757-fix-polling-for-events.yml
new file mode 100644
index 00000000000..10accb9ba4d
--- /dev/null
+++ b/changelogs/unreleased/218757-fix-polling-for-events.yml
@@ -0,0 +1,5 @@
+---
+title: Fix polling for resource events
+merge_request: 33025
+author:
+type: fixed
diff --git a/changelogs/unreleased/Remove-removeAllAssignees-logic-from-issue-model.yml b/changelogs/unreleased/Remove-removeAllAssignees-logic-from-issue-model.yml
new file mode 100644
index 00000000000..df0488a3882
--- /dev/null
+++ b/changelogs/unreleased/Remove-removeAllAssignees-logic-from-issue-model.yml
@@ -0,0 +1,5 @@
+---
+title: Remove removeAllAssignees logic from issue model
+merge_request: 32247
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Remove-removeAssignee-logic-from-issue-model.yml b/changelogs/unreleased/Remove-removeAssignee-logic-from-issue-model.yml
new file mode 100644
index 00000000000..257f95de712
--- /dev/null
+++ b/changelogs/unreleased/Remove-removeAssignee-logic-from-issue-model.yml
@@ -0,0 +1,5 @@
+---
+title: Remove removeAssignee logic from issue model
+merge_request: 32248
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/ab-services-partial-indexes.yml b/changelogs/unreleased/ab-services-partial-indexes.yml
new file mode 100644
index 00000000000..15dad096f7b
--- /dev/null
+++ b/changelogs/unreleased/ab-services-partial-indexes.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust condition for partial indexes on services table
+merge_request: 33044
+author:
+type: performance
diff --git a/changelogs/unreleased/add-api-endpoint-for-resource-milestone-events-pd.yml b/changelogs/unreleased/add-api-endpoint-for-resource-milestone-events-pd.yml
new file mode 100644
index 00000000000..8f5ef68ba5b
--- /dev/null
+++ b/changelogs/unreleased/add-api-endpoint-for-resource-milestone-events-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint for resource milestone events
+merge_request: 31720
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-import-export-tmp-folder-cleanup.yml b/changelogs/unreleased/georgekoltsov-import-export-tmp-folder-cleanup.yml
new file mode 100644
index 00000000000..d79f751a48f
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-import-export-tmp-folder-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up shared/tmp folder after Import/Export
+merge_request: 32326
+author:
+type: fixed
diff --git a/db/migrate/20200526120714_change_partial_indexes_on_services.rb b/db/migrate/20200526120714_change_partial_indexes_on_services.rb
new file mode 100644
index 00000000000..a4d58cda105
--- /dev/null
+++ b/db/migrate/20200526120714_change_partial_indexes_on_services.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ChangePartialIndexesOnServices < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :services, [:type, :instance], unique: true, where: 'instance = true', name: 'index_services_on_type_and_instance_partial'
+ remove_concurrent_index_by_name :services, 'index_services_on_type_and_instance'
+
+ add_concurrent_index :services, [:type, :template], unique: true, where: 'template = true', name: 'index_services_on_type_and_template_partial'
+ remove_concurrent_index_by_name :services, 'index_services_on_type_and_template'
+ end
+
+ def down
+ add_concurrent_index :services, [:type, :instance], unique: true, where: 'instance IS TRUE', name: 'index_services_on_type_and_instance'
+ remove_concurrent_index_by_name :services, 'index_services_on_type_and_instance_partial'
+
+ add_concurrent_index :services, [:type, :template], unique: true, where: 'template IS TRUE', name: 'index_services_on_type_and_template'
+ remove_concurrent_index_by_name :services, 'index_services_on_type_and_template_partial'
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index ef61b7de4dd..09ee7544c72 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10647,9 +10647,9 @@ CREATE INDEX index_services_on_type ON public.services USING btree (type);
CREATE INDEX index_services_on_type_and_id_and_template_when_active ON public.services USING btree (type, id, template) WHERE (active = true);
-CREATE UNIQUE INDEX index_services_on_type_and_instance ON public.services USING btree (type, instance) WHERE (instance IS TRUE);
+CREATE UNIQUE INDEX index_services_on_type_and_instance_partial ON public.services USING btree (type, instance) WHERE (instance = true);
-CREATE UNIQUE INDEX index_services_on_type_and_template ON public.services USING btree (type, template) WHERE (template IS TRUE);
+CREATE UNIQUE INDEX index_services_on_type_and_template_partial ON public.services USING btree (type, template) WHERE (template = true);
CREATE UNIQUE INDEX index_shards_on_name ON public.shards USING btree (name);
@@ -13934,5 +13934,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200519171058
20200525114553
20200525121014
+20200526120714
\.
diff --git a/doc/api/instance_level_ci_variables.md b/doc/api/instance_level_ci_variables.md
index d0871fdf4a7..3acd1652e1c 100644
--- a/doc/api/instance_level_ci_variables.md
+++ b/doc/api/instance_level_ci_variables.md
@@ -77,7 +77,7 @@ POST /admin/ci/variables
| Attribute | Type | required | Description |
|-----------------|---------|----------|-----------------------|
| `key` | string | yes | The `key` of a variable. Max 255 characters, only `A-Z`, `a-z`, `0-9`, and `_` are allowed. |
-| `value` | string | yes | The `value` of a variable. |
+| `value` | string | yes | The `value` of a variable. Around 700 characters allowed. |
| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file`. |
| `protected` | boolean | no | Whether the variable is protected. |
| `masked` | boolean | no | Whether the variable is masked. |
diff --git a/doc/api/resource_milestone_events.md b/doc/api/resource_milestone_events.md
new file mode 100644
index 00000000000..695687ada6d
--- /dev/null
+++ b/doc/api/resource_milestone_events.md
@@ -0,0 +1,224 @@
+---
+stage: Plan
+group: Project Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
+# Resource milestone events API
+
+Resource milestone events keep track of what happens to GitLab [issues](../user/project/issues/),
+[merge requests](../user/project/merge_requests/), and [epics](../user/group/epics/).
+
+Use them to track which milestone was added or removed, who did it, and when it happened.
+
+## Issues
+
+### List project issue milestone events
+
+Gets a list of all milestone events for a single issue.
+
+```plaintext
+GET /projects/:id/issues/:issue_iid/resource_milestone_events
+```
+
+| Attribute | Type | Required | Description |
+| ----------- | -------------- | -------- | ------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `issue_iid` | integer | yes | The IID of an issue |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_milestone_events"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 142,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-20T13:38:20.077Z",
+ "resource_type": "Issue",
+ "resource_id": 253,
+ "milestone": {
+ "id": 61,
+ "iid": 9,
+ "project_id": 7,
+ "title": "v1.2",
+ "description": "Ipsum Lorem",
+ "state": "active",
+ "created_at": "2020-01-27T05:07:12.573Z",
+ "updated_at": "2020-01-27T05:07:12.573Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://gitlab.example.com:3000/group/project/-/milestones/9"
+ },
+ "action": "add"
+ },
+ {
+ "id": 143,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-21T14:38:20.077Z",
+ "resource_type": "Issue",
+ "resource_id": 253,
+ "milestone": {
+ "id": 61,
+ "iid": 9,
+ "project_id": 7,
+ "title": "v1.2",
+ "description": "Ipsum Lorem",
+ "state": "active",
+ "created_at": "2020-01-27T05:07:12.573Z",
+ "updated_at": "2020-01-27T05:07:12.573Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://gitlab.example.com:3000/group/project/-/milestones/9"
+ },
+ "action": "remove"
+ }
+]
+```
+
+### Get single issue milestone event
+
+Returns a single milestone event for a specific project issue
+
+```plaintext
+GET /projects/:id/issues/:issue_iid/resource_milestone_events/:resource_milestone_event_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
+| `issue_iid` | integer | yes | The IID of an issue |
+| `resource_milestone_event_id` | integer | yes | The ID of a milestone event |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_milestone_events/1"
+```
+
+## Merge requests
+
+### List project merge request milestone events
+
+Gets a list of all milestone events for a single merge request.
+
+```plaintext
+GET /projects/:id/merge_requests/:merge_request_iid/resource_milestone_events
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_milestone_events"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 142,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-20T13:38:20.077Z",
+ "resource_type": "MergeRequest",
+ "resource_id": 142,
+ "milestone": {
+ "id": 61,
+ "iid": 9,
+ "project_id": 7,
+ "title": "v1.2",
+ "description": "Ipsum Lorem",
+ "state": "active",
+ "created_at": "2020-01-27T05:07:12.573Z",
+ "updated_at": "2020-01-27T05:07:12.573Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://gitlab.example.com:3000/group/project/-/milestones/9"
+ },
+ "action": "add"
+ },
+ {
+ "id": 143,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-21T14:38:20.077Z",
+ "resource_type": "MergeRequest",
+ "resource_id": 142,
+ "milestone": {
+ "id": 61,
+ "iid": 9,
+ "project_id": 7,
+ "title": "v1.2",
+ "description": "Ipsum Lorem",
+ "state": "active",
+ "created_at": "2020-01-27T05:07:12.573Z",
+ "updated_at": "2020-01-27T05:07:12.573Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://gitlab.example.com:3000/group/project/-/milestones/9"
+ },
+ "action": "remove"
+ }
+]
+```
+
+### Get single merge request milestone event
+
+Returns a single milestone event for a specific project merge request
+
+```plaintext
+GET /projects/:id/merge_requests/:merge_request_iid/resource_milestone_events/:resource_milestone_event_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `resource_milestone_event_id` | integer | yes | The ID of a milestone event |
+
+Example request:
+
+```shell
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_milestone_events/120"
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index b8135539cda..cdfdc7f92dd 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -170,6 +170,7 @@ module API
mount ::API::Notes
mount ::API::Discussions
mount ::API::ResourceLabelEvents
+ mount ::API::ResourceMilestoneEvents
mount ::API::NotificationSettings
mount ::API::Pages
mount ::API::PagesDomains
diff --git a/lib/api/entities/resource_milestone_event.rb b/lib/api/entities/resource_milestone_event.rb
new file mode 100644
index 00000000000..26dc6620cbe
--- /dev/null
+++ b/lib/api/entities/resource_milestone_event.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ResourceMilestoneEvent < Grape::Entity
+ expose :id
+ expose :user, using: Entities::UserBasic
+ expose :created_at
+ expose :resource_type do |event, _options|
+ event.issuable.class.name
+ end
+ expose :resource_id do |event, _options|
+ event.issuable.id
+ end
+ expose :milestone, using: Entities::Milestone
+ expose :action
+ expose :state
+ end
+ end
+end
diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb
new file mode 100644
index 00000000000..30ff5a9b4be
--- /dev/null
+++ b/lib/api/resource_milestone_events.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module API
+ class ResourceMilestoneEvents < Grape::API
+ include PaginationParams
+ helpers ::API::Helpers::NotesHelpers
+
+ before { authenticate! }
+
+ [Issue, MergeRequest].each do |eventable_type|
+ parent_type = eventable_type.parent_class.to_s.underscore
+ eventables_str = eventable_type.to_s.underscore.pluralize
+
+ params do
+ requires :id, type: String, desc: "The ID of a #{parent_type}"
+ end
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "Get a list of #{eventable_type.to_s.downcase} resource milestone events" do
+ success Entities::ResourceMilestoneEvent
+ end
+ params do
+ requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
+ use :pagination
+ end
+
+ get ":id/#{eventables_str}/:eventable_id/resource_milestone_events" do
+ eventable = find_noteable(eventable_type, params[:eventable_id])
+
+ opts = { page: params[:page], per_page: params[:per_page] }
+ events = ResourceMilestoneEventFinder.new(current_user, eventable, opts).execute
+
+ present paginate(events), with: Entities::ResourceMilestoneEvent
+ end
+
+ desc "Get a single #{eventable_type.to_s.downcase} resource milestone event" do
+ success Entities::ResourceMilestoneEvent
+ end
+ params do
+ requires :event_id, type: String, desc: 'The ID of a resource milestone event'
+ requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
+ end
+ get ":id/#{eventables_str}/:eventable_id/resource_milestone_events/:event_id" do
+ eventable = find_noteable(eventable_type, params[:eventable_id])
+
+ event = eventable.resource_milestone_events.find(params[:event_id])
+
+ not_found!('ResourceMilestoneEvent') unless can?(current_user, :read_milestone, event.milestone_parent)
+
+ present event, with: Entities::ResourceMilestoneEvent
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index b1219384732..d5816100023 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -26,6 +26,7 @@ module Gitlab
rescue => e
raise Projects::ImportService::Error.new(e.message)
ensure
+ remove_base_tmp_dir
remove_import_file
end
@@ -148,6 +149,10 @@ module Gitlab
::Project.find_by_full_path("#{project.namespace.full_path}/#{original_path}")
end
end
+
+ def remove_base_tmp_dir
+ FileUtils.rm_rf(@shared.base_path)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index bd69673ecdf..e4724659eff 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -16,8 +16,6 @@ module Gitlab
def save
if compress_and_save
- remove_export_path
-
Gitlab::Export::Logger.info(
message: 'Export archive saved',
exportable_class: @exportable.class.to_s,
@@ -33,8 +31,7 @@ module Gitlab
@shared.error(e)
false
ensure
- remove_archive
- remove_export_path
+ remove_base_tmp_dir
end
private
@@ -43,12 +40,8 @@ module Gitlab
tar_czf(archive: archive_file, dir: @shared.export_path)
end
- def remove_export_path
- FileUtils.rm_rf(@shared.export_path)
- end
-
- def remove_archive
- FileUtils.rm_rf(@shared.archive_path)
+ def remove_base_tmp_dir
+ FileUtils.rm_rf(@shared.base_path)
end
def archive_file
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1ce0888b764..c4e5a217833 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -23566,9 +23566,6 @@ msgstr ""
msgid "Usage ping is not enabled"
msgstr ""
-msgid "Usage quotas help link"
-msgstr ""
-
msgid "Usage statistics"
msgstr ""
@@ -23578,12 +23575,18 @@ msgstr ""
msgid "UsageQuota|Artifacts"
msgstr ""
+msgid "UsageQuota|Build Artifacts"
+msgstr ""
+
msgid "UsageQuota|Buy additional minutes"
msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
+msgid "UsageQuota|LFS Objects"
+msgstr ""
+
msgid "UsageQuota|LFS Storage"
msgstr ""
@@ -23593,12 +23596,18 @@ msgstr ""
msgid "UsageQuota|Pipelines"
msgstr ""
+msgid "UsageQuota|Repositories"
+msgstr ""
+
msgid "UsageQuota|Repository"
msgstr ""
msgid "UsageQuota|Storage"
msgstr ""
+msgid "UsageQuota|Storage usage:"
+msgstr ""
+
msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr ""
@@ -23617,12 +23626,18 @@ msgstr ""
msgid "UsageQuota|Usage of resources across your projects"
msgstr ""
+msgid "UsageQuota|Usage quotas help link"
+msgstr ""
+
msgid "UsageQuota|Usage since"
msgstr ""
msgid "UsageQuota|Wiki"
msgstr ""
+msgid "UsageQuota|Wikis"
+msgstr ""
+
msgid "Use %{code_start}::%{code_end} to create a %{link_start}scoped label set%{link_end} (eg. %{code_start}priority::1%{code_end})"
msgstr ""
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 39594ff287d..f883c02af65 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -37,7 +37,7 @@ describe Projects::NotesController do
project.add_developer(user)
end
- it 'passes last_fetched_at from headers to NotesFinder' do
+ it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do
last_fetched_at = 3.hours.ago.to_i
request.headers['X-Last-Fetched-At'] = last_fetched_at
@@ -46,6 +46,10 @@ describe Projects::NotesController do
.with(anything, hash_including(last_fetched_at: last_fetched_at))
.and_call_original
+ expect(ResourceEvents::MergeIntoNotesService).to receive(:new)
+ .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
+ .and_call_original
+
get :index, params: request_params
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 8db7a571c62..00d69860665 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -26,10 +26,6 @@ describe Projects::PipelinesController do
context 'when using persisted stages', :request_store do
render_views
- before do
- stub_feature_flags(ci_pipeline_persisted_stages: true)
- end
-
it 'returns serialized pipelines' do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
@@ -66,46 +62,6 @@ describe Projects::PipelinesController do
end
end
- context 'when using legacy stages', :request_store do
- before do
- stub_feature_flags(ci_pipeline_persisted_stages: false)
- end
-
- it 'returns JSON with serialized pipelines' do
- get_pipelines_index_json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('pipeline')
-
- expect(json_response).to include('pipelines')
- expect(json_response['pipelines'].count).to eq 6
- expect(json_response['count']['all']).to eq '6'
- expect(json_response['count']['running']).to eq '2'
- expect(json_response['count']['pending']).to eq '1'
- expect(json_response['count']['finished']).to eq '3'
-
- json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
- expect(stages.count).to eq 3
- end
- end
-
- it 'does not execute N+1 queries' do
- get_pipelines_index_json
-
- control_count = ActiveRecord::QueryRecorder.new do
- get_pipelines_index_json
- end.count
-
- create_all_pipeline_types
-
- # There appears to be one extra query for Pipelines#has_warnings? for some reason
- expect { get_pipelines_index_json }.not_to exceed_query_limit(control_count + 1)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['pipelines'].count).to eq 12
- end
- end
-
it 'does not include coverage data for the pipelines' do
get_pipelines_index_json
diff --git a/spec/finders/resource_milestone_event_finder_spec.rb b/spec/finders/resource_milestone_event_finder_spec.rb
new file mode 100644
index 00000000000..fa7fda37849
--- /dev/null
+++ b/spec/finders/resource_milestone_event_finder_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceMilestoneEventFinder do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue_project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: issue_project) }
+
+ describe '#execute' do
+ subject { described_class.new(user, issue).execute }
+
+ it 'returns events with milestones accessible by user' do
+ milestone = create(:milestone, project: issue_project)
+ event = create_event(milestone)
+ issue_project.add_guest(user)
+
+ expect(subject).to eq [event]
+ end
+
+ it 'filters events with public project milestones if issues and MRs are private' do
+ project = create(:project, :public, :issues_private, :merge_requests_private)
+ milestone = create(:milestone, project: project)
+ create_event(milestone)
+
+ expect(subject).to be_empty
+ end
+
+ it 'filters events with project milestones not accessible by user' do
+ project = create(:project, :private)
+ milestone = create(:milestone, project: project)
+ create_event(milestone)
+
+ expect(subject).to be_empty
+ end
+
+ it 'filters events with group milestones not accessible by user' do
+ group = create(:group, :private)
+ milestone = create(:milestone, group: group)
+ create_event(milestone)
+
+ expect(subject).to be_empty
+ end
+
+ it 'paginates results' do
+ milestone = create(:milestone, project: issue_project)
+ create_event(milestone)
+ create_event(milestone)
+ issue_project.add_guest(user)
+
+ paginated = described_class.new(user, issue, per_page: 1).execute
+
+ expect(subject.count).to eq 2
+ expect(paginated.count).to eq 1
+ end
+
+ context 'when multiple events share the same milestone' do
+ it 'avoids N+1 queries' do
+ issue_project.add_developer(user)
+
+ milestone1 = create(:milestone, project: issue_project)
+ milestone2 = create(:milestone, project: issue_project)
+
+ control_count = ActiveRecord::QueryRecorder.new { described_class.new(user, issue).execute }.count
+ expect(control_count).to eq(1) # 1 events query
+
+ create_event(milestone1, :add)
+ create_event(milestone1, :remove)
+ create_event(milestone1, :add)
+ create_event(milestone1, :remove)
+ create_event(milestone2, :add)
+ create_event(milestone2, :remove)
+
+ # 1 events + 1 milestones + 1 project + 1 user + 4 ability
+ expect { described_class.new(user, issue).execute }.not_to exceed_query_limit(control_count + 7)
+ end
+ end
+
+ def create_event(milestone, action = :add)
+ create(:resource_milestone_event, issue: issue, milestone: milestone, action: action)
+ end
+ end
+end
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
index 64d59884594..b1dff3d8ebb 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -5,6 +5,11 @@ import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import createFlash from '~/flash';
import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ trackAlertsDetailsViewsOptions,
+ trackAlertStatusUpdateOptions,
+} from '~/alert_management/constants';
+import Tracking from '~/tracking';
import mockAlerts from '../mocks/alerts.json';
@@ -253,7 +258,7 @@ describe('AlertDetails', () => {
});
});
- describe('updating the alert status', () => {
+ describe('Updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
@@ -298,4 +303,31 @@ describe('AlertDetails', () => {
});
});
});
+
+ describe('Snowplow tracking', () => {
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alert: mockAlert },
+ loading: false,
+ });
+ });
+
+ it('should track alert details page views', () => {
+ const { category, action } = trackAlertsDetailsViewsOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+
+ it('should track alert status updates', () => {
+ Tracking.event.mockClear();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+ findStatusDropdownItem().vm.$emit('click');
+ const status = findStatusDropdownItem().text();
+ setImmediate(() => {
+ const { category, action, label } = trackAlertStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
+ });
+ });
+ });
});
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index 0f18600a736..88dde9f16f7 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -14,9 +14,14 @@ import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash from '~/flash';
import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
-import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants';
+import {
+ ALERTS_STATUS_TABS,
+ trackAlertListViewsOptions,
+ trackAlertStatusUpdateOptions,
+} from '~/alert_management/constants';
import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql';
import mockAlerts from '../mocks/alerts.json';
+import Tracking from '~/tracking';
jest.mock('~/flash');
@@ -94,7 +99,7 @@ describe('AlertManagementList', () => {
}
});
- describe('alert management feature renders empty state', () => {
+ describe('Empty state', () => {
it('shows empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
@@ -137,7 +142,7 @@ describe('AlertManagementList', () => {
findAlerts()
.at(0)
.classes(),
- ).not.toContain('hover-bg-blue-50');
+ ).not.toContain('gl-hover-bg-blue-50');
});
it('error state', () => {
@@ -154,7 +159,7 @@ describe('AlertManagementList', () => {
findAlerts()
.at(0)
.classes(),
- ).not.toContain('hover-bg-blue-50');
+ ).not.toContain('gl-hover-bg-blue-50');
});
it('empty state', () => {
@@ -171,7 +176,7 @@ describe('AlertManagementList', () => {
findAlerts()
.at(0)
.classes(),
- ).not.toContain('hover-bg-blue-50');
+ ).not.toContain('gl-hover-bg-blue-50');
});
it('has data state', () => {
@@ -187,7 +192,7 @@ describe('AlertManagementList', () => {
findAlerts()
.at(0)
.classes(),
- ).toContain('hover-bg-blue-50');
+ ).toContain('gl-hover-bg-blue-50');
});
it('displays status dropdown', () => {
@@ -363,4 +368,31 @@ describe('AlertManagementList', () => {
});
});
});
+
+ describe('Snowplow tracking', () => {
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, alertsCount },
+ loading: false,
+ });
+ });
+
+ it('should track alert list page views', () => {
+ const { category, action } = trackAlertListViewsOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+
+ it('should track alert status updates', () => {
+ Tracking.event.mockClear();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+ findFirstStatusOption().vm.$emit('click');
+ const status = findFirstStatusOption().text();
+ setImmediate(() => {
+ const { category, action, label } = trackAlertStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
+ });
+ });
+ });
});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index adbbc04ce78..03181e322a1 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -18,6 +18,12 @@ import {
severityLevelVariant,
errorStatus,
} from '~/error_tracking/components/constants';
+import Tracking from '~/tracking';
+import {
+ trackClickErrorLinkToSentryOptions,
+ trackErrorDetailsViewsOptions,
+ trackErrorStatusUpdateOptions,
+} from '~/error_tracking/utils';
jest.mock('~/flash');
@@ -30,12 +36,19 @@ describe('ErrorDetails', () => {
let actions;
let getters;
let mocks;
+ const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
const findInput = name => {
const inputs = wrapper.findAll(GlFormInput).filter(c => c.attributes('name') === name);
return inputs.length ? inputs.at(0) : inputs;
};
+ const findUpdateIgnoreStatusButton = () =>
+ wrapper.find('[data-testid="update-ignore-status-btn"]');
+ const findUpdateResolveStatusButton = () =>
+ wrapper.find('[data-testid="update-resolve-status-btn"]');
+ const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]');
+
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
stubs: { GlDeprecatedButton, GlSprintf },
@@ -57,7 +70,7 @@ describe('ErrorDetails', () => {
beforeEach(() => {
actions = {
startPollingStacktrace: () => {},
- updateIgnoreStatus: jest.fn(),
+ updateIgnoreStatus: jest.fn().mockResolvedValue({}),
updateResolveStatus: jest.fn().mockResolvedValue({ closed_issue_iid: 1 }),
};
@@ -302,11 +315,6 @@ describe('ErrorDetails', () => {
});
describe('Status update', () => {
- const findUpdateIgnoreStatusButton = () =>
- wrapper.find('[data-qa-selector="update_ignore_status_button"]');
- const findUpdateResolveStatusButton = () =>
- wrapper.find('[data-qa-selector="update_resolve_status_button"]');
-
afterEach(() => {
actions.updateIgnoreStatus.mockClear();
actions.updateResolveStatus.mockClear();
@@ -491,4 +499,55 @@ describe('ErrorDetails', () => {
});
});
});
+
+ describe('Snowplow tracking', () => {
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ mocks.$apollo.queries.error.loading = false;
+ mountComponent();
+ wrapper.setData({
+ error: { externalUrl },
+ });
+ });
+
+ it('should track detail page views', () => {
+ const { category, action } = trackErrorDetailsViewsOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+
+ it('should track IGNORE status update', () => {
+ Tracking.event.mockClear();
+ findUpdateIgnoreStatusButton().vm.$emit('click');
+ setImmediate(() => {
+ const { category, action, label } = trackErrorStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, {
+ label,
+ property: 'ignored',
+ });
+ });
+ });
+
+ it('should track RESOLVE status update', () => {
+ Tracking.event.mockClear();
+ findUpdateResolveStatusButton().vm.$emit('click');
+ setImmediate(() => {
+ const { category, action, label } = trackErrorStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, {
+ label,
+ property: 'resolved',
+ });
+ });
+ });
+
+ it('should track external Sentry link views', () => {
+ Tracking.event.mockClear();
+ findExternalUrl().trigger('click');
+ setImmediate(() => {
+ const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
+ externalUrl,
+ );
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
+ });
+ });
+ });
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index a6cb074f481..b5e09c86d08 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -4,7 +4,9 @@ import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } fr
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
+import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
import errorsList from './list_mock.json';
+import Tracking from '~/tracking';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -460,4 +462,41 @@ describe('ErrorTrackingList', () => {
});
});
});
+
+ describe('Snowplow tracking', () => {
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ store.state.list.loading = false;
+ store.state.list.errors = errorsList;
+ mountComponent({
+ stubs: {
+ GlTable: false,
+ GlLink: false,
+ GlDeprecatedButton: false,
+ },
+ });
+ });
+
+ it('should track list views', () => {
+ const { category, action } = trackErrorListViewsOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+
+ it('should track status updates', () => {
+ Tracking.event.mockClear();
+ const status = 'ignored';
+ findErrorActions().vm.$emit('update-issue-status', {
+ errorId: 1,
+ status,
+ });
+
+ setImmediate(() => {
+ const { category, action, label } = trackErrorStatusUpdateOptions;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, {
+ label,
+ property: status,
+ });
+ });
+ });
+ });
});
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 60179146416..b8c34a461ef 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -18,6 +18,7 @@ describe Gitlab::ImportExport::Importer do
FileUtils.mkdir_p(shared.export_path)
ImportExportUpload.create(project: project, import_file: import_file)
+ allow(FileUtils).to receive(:rm_rf).and_call_original
end
after do
@@ -78,6 +79,13 @@ describe Gitlab::ImportExport::Importer do
expect(project.import_export_upload.import_file&.file).to be_nil
end
+ it 'removes tmp files' do
+ importer.execute
+
+ expect(FileUtils).to have_received(:rm_rf).with(shared.base_path)
+ expect(Dir.exist?(shared.base_path)).to eq(false)
+ end
+
it 'sets the correct visibility_level when visibility level is a string' do
project.create_or_update_import_data(
data: { override_params: { visibility_level: Gitlab::VisibilityLevel::PRIVATE.to_s } }
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index a59cf7a1260..18e9d7da32d 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -5,18 +5,21 @@ require 'fileutils'
describe Gitlab::ImportExport::Saver do
let!(:project) { create(:project, :public, name: 'project') }
- let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:base_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{base_path}/project_tree_saver_spec/export" }
let(:shared) { project.import_export_shared }
subject { described_class.new(exportable: project, shared: shared) }
before do
+ allow(shared).to receive(:base_path).and_return(base_path)
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path)
end
FileUtils.mkdir_p(shared.export_path)
FileUtils.touch("#{shared.export_path}/tmp.bundle")
+ allow(FileUtils).to receive(:rm_rf).and_call_original
end
after do
@@ -31,4 +34,11 @@ describe Gitlab::ImportExport::Saver do
expect(ImportExportUpload.find_by(project: project).export_file.url)
.to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
end
+
+ it 'removes tmp files' do
+ subject.save
+
+ expect(FileUtils).to have_received(:rm_rf).with(base_path)
+ expect(Dir.exist?(base_path)).to eq(false)
+ end
end
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
index 4ad168ff0f2..b887a06c5af 100644
--- a/spec/models/ci/instance_variable_spec.rb
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -9,6 +9,7 @@ describe Ci::InstanceVariable do
it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
+ it { is_expected.to validate_length_of(:encrypted_value).is_at_most(1024).with_message(/Variables over 700 characters risk exceeding the limit/) }
describe '.unprotected' do
subject { described_class.unprotected }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 687ae71cdab..4a954be047f 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1007,19 +1007,6 @@ describe Ci::Pipeline, :mailer do
subject { pipeline.ordered_stages }
- context 'when using legacy stages' do
- before do
- stub_feature_flags(
- ci_pipeline_persisted_stages: false,
- ci_atomic_processing: false
- )
- end
-
- it 'returns legacy stages in valid order' do
- expect(subject.map(&:name)).to eq %w[build test]
- end
- end
-
context 'when using atomic processing' do
before do
stub_feature_flags(
@@ -1051,7 +1038,6 @@ describe Ci::Pipeline, :mailer do
context 'when using persisted stages' do
before do
stub_feature_flags(
- ci_pipeline_persisted_stages: true,
ci_atomic_processing: false
)
end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index 3f8d8b4c1df..66686ec77d0 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -95,4 +95,34 @@ describe ResourceMilestoneEvent, type: :model do
end
end
end
+
+ describe '#milestone_parent' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
+ let(:milestone) { create(:milestone, project: project) }
+ let(:event) { create(:resource_milestone_event, milestone: milestone) }
+
+ context 'when milestone parent is project' do
+ it 'returns the expected parent' do
+ expect(event.milestone_parent).to eq(project)
+ end
+ end
+
+ context 'when milestone parent is group' do
+ let(:milestone) { create(:milestone, group: group) }
+
+ it 'returns the expected parent' do
+ expect(event.milestone_parent).to eq(group)
+ end
+ end
+
+ context 'when milestone is nil' do
+ let(:event) { create(:resource_milestone_event, milestone: nil) }
+
+ it 'returns nil' do
+ expect(event.milestone_parent).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
index bc2f0ba50a2..185fde17e1b 100644
--- a/spec/requests/api/admin/ci/variables_spec.rb
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -109,6 +109,22 @@ describe ::API::Admin::Ci::Variables do
expect(response).to have_gitlab_http_status(:bad_request)
end
+
+ it 'does not allow values above 700 characters' do
+ too_long_message = <<~MESSAGE.strip
+ The encrypted value of the provided variable exceeds 1024 bytes. \
+ Variables over 700 characters risk exceeding the limit.
+ MESSAGE
+
+ expect do
+ post api('/admin/ci/variables', admin),
+ params: { key: 'too_long', value: SecureRandom.hex(701) }
+ end.not_to change { ::Ci::InstanceVariable.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match('message' =>
+ a_hash_including('encrypted_value' => [too_long_message]))
+ end
end
context 'authorized user with invalid permissions' do
diff --git a/spec/requests/api/resource_milestone_events_spec.rb b/spec/requests/api/resource_milestone_events_spec.rb
new file mode 100644
index 00000000000..b2e92fde5ee
--- /dev/null
+++ b/spec/requests/api/resource_milestone_events_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::ResourceMilestoneEvents do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project, :public, namespace: user.namespace) }
+ let!(:milestone) { create(:milestone, project: project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when eventable is an Issue' do
+ it_behaves_like 'resource_milestone_events API', 'projects', 'issues', 'iid' do
+ let(:parent) { project }
+ let(:eventable) { create(:issue, project: project, author: user) }
+ end
+ end
+
+ context 'when eventable is a Merge Request' do
+ it_behaves_like 'resource_milestone_events API', 'projects', 'merge_requests', 'iid' do
+ let(:parent) { project }
+ let(:eventable) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 4e4cc9c35e6..c8f25423f85 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -10,10 +10,6 @@ describe PipelineSerializer do
described_class.new(current_user: user, project: project)
end
- before do
- stub_feature_flags(ci_pipeline_persisted_stages: true)
- end
-
subject { serializer.represent(resource) }
describe '#represent' do
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index 4576c786416..ea49b26cc7c 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -134,7 +134,7 @@ describe Groups::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
- expect(File.exist?(shared.archive_path)).to eq(false)
+ expect(Dir.exist?(shared.base_path)).to eq(false)
end
it 'notifies the user about failed group export' do
@@ -159,7 +159,7 @@ describe Groups::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
- expect(File.exist?(shared.archive_path)).to eq(false)
+ expect(Dir.exist?(shared.base_path)).to eq(false)
end
it 'notifies logger' do
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index b1eae057c47..065be36c1dc 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -91,6 +91,7 @@ describe Groups::ImportExport::ImportService do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:error)
allow(import_logger).to receive(:info)
+ allow(FileUtils).to receive(:rm_rf).and_call_original
end
context 'when user has correct permissions' do
@@ -104,6 +105,16 @@ describe Groups::ImportExport::ImportService do
expect(group.import_export_upload.import_file.file).to be_nil
end
+ it 'removes tmp files' do
+ shared = Gitlab::ImportExport::Shared.new(group)
+ allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
+
+ subject
+
+ expect(FileUtils).to have_received(:rm_rf).with(shared.base_path)
+ expect(Dir.exist?(shared.base_path)).to eq(false)
+ end
+
it 'logs the import success' do
expect(import_logger).to receive(:info).with(
group_id: group.id,
@@ -191,6 +202,7 @@ describe Groups::ImportExport::ImportService do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:error)
allow(import_logger).to receive(:info)
+ allow(FileUtils).to receive(:rm_rf).and_call_original
end
context 'when user has correct permissions' do
@@ -204,6 +216,16 @@ describe Groups::ImportExport::ImportService do
expect(group.import_export_upload.import_file.file).to be_nil
end
+ it 'removes tmp files' do
+ shared = Gitlab::ImportExport::Shared.new(group)
+ allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
+
+ subject
+
+ expect(FileUtils).to have_received(:rm_rf).with(shared.base_path)
+ expect(Dir.exist?(shared.base_path)).to eq(false)
+ end
+
it 'logs the import success' do
expect(import_logger).to receive(:info).with(
group_id: group.id,
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index e52ac02a2cc..6e584cb44e2 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
if resource_name == 'issue'
- it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/218757' do
+ it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
find("#{form_selector} .note-textarea").send_keys(comment)
click_button 'Comment & close issue'
@@ -206,7 +206,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
if resource_name == 'issue'
- it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/218757' do
+ it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}" do
click_button 'Start thread & close issue'
expect(page).to have_content(comment)
diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb
new file mode 100644
index 00000000000..bca51dab353
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events" do
+ let!(:event) { create_event(milestone) }
+
+ it "returns an array of resource milestone events" do
+ url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events"
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(event.id)
+ expect(json_response.first['milestone']['id']).to eq(event.milestone.id)
+ expect(json_response.first['action']).to eq(event.action)
+ end
+
+ it "returns a 404 error when eventable id not found" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events/:event_id" do
+ let!(:event) { create_event(milestone) }
+
+ it "returns a resource milestone event by id" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(event.id)
+ expect(json_response['milestone']['id']).to eq(event.milestone.id)
+ expect(json_response['action']).to eq(event.action)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns a 404 error if resource milestone event not found" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{non_existing_record_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def create_event(milestone, action: :add)
+ create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action)
+ end
+end