Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/content_editor/services/upload_file.js44
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js4
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issue_card_time_info.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue8
-rw-r--r--app/assets/javascripts/issues_list/constants.js2
-rw-r--r--app/assets/javascripts/issues_list/index.js2
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue11
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue39
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/lib/graphql.js52
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js32
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql12
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js13
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue35
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue3
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue20
-rw-r--r--app/assets/javascripts/repository/constants.js3
-rw-r--r--app/assets/stylesheets/utilities.scss10
-rw-r--r--app/graphql/resolvers/ci/template_resolver.rb2
-rw-r--r--app/helpers/ci/jobs_helper.rb1
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb8
-rw-r--r--app/models/award_emoji.rb11
-rw-r--r--app/models/error_tracking/error_event.rb2
-rw-r--r--app/validators/json_schemas/error_tracking_event_payload.json111
-rw-r--r--config/feature_flags/development/api_caching_rate_limit_repository_compare.yml2
-rw-r--r--config/feature_flags/development/api_caching_repository_compare.yml8
-rw-r--r--danger/gitaly/Dangerfile22
-rw-r--r--db/migrate/20210701111627_add_upvotes_count_to_issues.rb15
-rw-r--r--db/migrate/20210708131048_add_error_tracking_counter_cache.rb11
-rw-r--r--db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb26
-rw-r--r--db/schema_migrations/202107011116271
-rw-r--r--db/schema_migrations/202107011119091
-rw-r--r--db/schema_migrations/202107081310481
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/user/permissions.md6
-rw-r--r--lib/api/repositories.rb8
-rw-r--r--lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb40
-rw-r--r--lib/gitlab/import_export/project/import_export.yml1
-rw-r--r--locale/gitlab.pot21
-rw-r--r--package.json2
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb10
-rw-r--r--spec/frontend/content_editor/services/upload_file_spec.js46
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js2
-rw-r--r--spec/frontend/jobs/components/empty_state_spec.js1
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js1
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js15
-rw-r--r--spec/frontend/lib/graphql_spec.js54
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js85
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js21
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js29
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js59
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js58
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb46
-rw-r--r--spec/migrations/backfill_issues_upvotes_count_spec.rb35
-rw-r--r--spec/models/award_emoji_spec.rb39
-rw-r--r--spec/requests/api/repositories_spec.rb11
-rw-r--r--spec/requests/api/wikis_spec.rb3
-rw-r--r--spec/tooling/danger/project_helper_spec.rb2
-rw-r--r--tooling/danger/project_helper.rb1
-rw-r--r--yarn.lock8
68 files changed, 869 insertions, 295 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8be1d8ee85d..ad92618e1b4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-5fdd1ba64d79df3a46c74f29d17faf7927650887
+c8a29dc9fd507cab8835b2e1152b94a6ac96de35
diff --git a/app/assets/javascripts/content_editor/services/upload_file.js b/app/assets/javascripts/content_editor/services/upload_file.js
new file mode 100644
index 00000000000..613c53144a1
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/upload_file.js
@@ -0,0 +1,44 @@
+import axios from '~/lib/utils/axios_utils';
+
+const extractAttachmentLinkUrl = (html) => {
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+ const link = body.querySelector('a');
+ const src = link.getAttribute('href');
+ const { canonicalSrc } = link.dataset;
+
+ return { src, canonicalSrc };
+};
+
+/**
+ * Uploads a file with a post request to the URL indicated
+ * in the uploadsPath parameter. The expected response of the
+ * uploads service is a JSON object that contains, at least, a
+ * link property. The link property should contain markdown link
+ * definition (i.e. [GitLab](https://gitlab.com)).
+ *
+ * This Markdown will be rendered to extract its canonical and full
+ * URLs using GitLab Flavored Markdown renderer in the backend.
+ *
+ * @param {Object} params
+ * @param {String} params.uploadsPath An absolute URL that points to a service
+ * that allows sending a file for uploading via POST request.
+ * @param {String} params.renderMarkdown A function that accepts a markdown string
+ * and returns a rendered version in HTML format.
+ * @param {File} params.file The file to upload
+ *
+ * @returns Returns an object with two properties:
+ *
+ * canonicalSrc: The URL as defined in the Markdown
+ * src: The absolute URL that points to the resource in the server
+ */
+export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
+ const formData = new FormData();
+ formData.append('file', file, file.name);
+
+ const { data } = await axios.post(uploadsPath, formData);
+ const { markdown } = data.link;
+ const rendered = await renderMarkdown(markdown);
+
+ return extractAttachmentLinkUrl(rendered);
+};
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index 8a66d4d11b7..a7d44322eb1 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -86,11 +86,11 @@ export default class GroupFilterableList extends FilterableList {
// Get option query param, also preserve currently applied query param
const sortParam = getParameterByName(
'sort',
- isOptionFilterBySort ? e.currentTarget.href : window.location.href,
+ isOptionFilterBySort ? e.currentTarget.search : window.location.search,
);
const archivedParam = getParameterByName(
'archived',
- isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
+ isOptionFilterByArchivedProjects ? e.currentTarget.search : window.location.search,
);
if (sortParam) {
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
index 48eadec1d5c..6e300831e00 100644
--- a/app/assets/javascripts/issuable/components/issuable_by_email.vue
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -36,7 +36,7 @@ export default {
default: null,
},
issuableType: {
- default: '',
+ default: 'issue',
},
emailsHelpPagePath: {
default: '',
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 70d73aca925..07492b0046c 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -115,7 +115,7 @@ export default {
{{ timeEstimate }}
</span>
<weight-count
- class="gl-display-none gl-sm-display-inline-block gl-mr-3"
+ class="issuable-weight gl-display-none gl-sm-display-inline-block gl-mr-3"
:weight="issue.weight"
/>
<issue-health-status
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index fcd31013fe5..e3cc43d2679 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -664,7 +664,7 @@ export default {
v-gl-tooltip
class="gl-display-none gl-sm-display-block"
:title="$options.i18n.relatedMergeRequests"
- data-testid="issuable-mr"
+ data-testid="merge-requests"
>
<gl-icon name="merge-request" />
{{ issuable.mergeRequestsCount }}
@@ -672,7 +672,7 @@ export default {
<li
v-if="issuable.upvotes"
v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
+ class="issuable-upvotes gl-display-none gl-sm-display-block"
:title="$options.i18n.upvotes"
data-testid="issuable-upvotes"
>
@@ -682,7 +682,7 @@ export default {
<li
v-if="issuable.downvotes"
v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
+ class="issuable-downvotes gl-display-none gl-sm-display-block"
:title="$options.i18n.downvotes"
data-testid="issuable-downvotes"
>
@@ -690,7 +690,7 @@ export default {
{{ issuable.downvotes }}
</li>
<blocking-issues-count
- class="gl-display-none gl-sm-display-block"
+ class="blocking-issues gl-display-none gl-sm-display-block"
:blocking-issues-count="issuable.blockedByCount"
:is-list-item="true"
/>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index e4b9136343e..d94d4b9a19a 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -97,7 +97,7 @@ export const i18n = {
relatedMergeRequests: __('Related merge requests'),
reorderError: __('An error occurred while reordering issues.'),
rssLabel: __('Subscribe to RSS feed'),
- searchPlaceholder: __('Search or filter results…'),
+ searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
};
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index dc73d8c7cc8..71ceb9bef55 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { IssuableType } from '~/issue_show/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
@@ -150,7 +149,6 @@ export function mountIssuesListApp() {
// For IssuableByEmail component
emailsHelpPagePath,
initialEmail,
- issuableType: IssuableType.Issue,
markdownHelpPath,
quickActionsHelpPath,
resetPath,
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 35b16d73cc7..e31c13f40b0 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -35,11 +35,6 @@ export default {
required: false,
default: false,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
action: {
type: Object,
required: false,
@@ -75,11 +70,7 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <manual-variables-form
- v-if="shouldRenderManualVariables"
- :action="action"
- :variables-settings-url="variablesSettingsUrl"
- />
+ <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
<div class="text-content">
<div v-if="action && !shouldRenderManualVariables" class="text-center">
<gl-link
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index be95001a396..fa9ee56c049 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -50,11 +50,6 @@ export default {
required: false,
default: null,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
deploymentHelpUrl: {
type: String,
required: false,
@@ -315,7 +310,6 @@ export default {
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
- :variables-settings-url="variablesSettingsUrl"
/>
<!-- EO empty state -->
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index d45012d2023..269551ff9aa 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,14 +1,16 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
GlButton,
+ GlLink,
+ GlSprintf,
},
props: {
action: {
@@ -24,11 +26,6 @@ export default {
);
},
},
- variablesSettingsUrl: {
- type: String,
- required: true,
- default: '',
- },
},
inputTypes: {
key: 'key',
@@ -37,6 +34,9 @@ export default {
i18n: {
keyPlaceholder: s__('CiVariables|Input variable key'),
valuePlaceholder: s__('CiVariables|Input variable value'),
+ formHelpText: s__(
+ 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
+ ),
},
data() {
return {
@@ -47,17 +47,8 @@ export default {
};
},
computed: {
- helpText() {
- return sprintf(
- s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
- {
- linkStart: `<a href="${this.variablesSettingsUrl}">`,
- linkEnd: '</a>',
- },
- false,
- );
+ variableSettings() {
+ return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
watch: {
@@ -188,8 +179,14 @@ export default {
</div>
</div>
</div>
- <div class="d-flex gl-mt-3 justify-content-center">
- <p class="text-muted" data-testid="form-help-text" v-html="helpText"></p>
+ <div class="gl-text-center gl-mt-3">
+ <gl-sprintf :message="$options.i18n.formHelpText">
+ <template #link="{ content }">
+ <gl-link :href="variableSettings" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
</div>
<div class="d-flex justify-content-center">
<gl-button
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 260190f5043..1fb6a6f9850 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -15,7 +15,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
@@ -41,7 +40,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index cec689a44ca..d91fc61ba21 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -2,12 +2,13 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
-import { createHttpLink } from 'apollo-link-http';
+import { HttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
+import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
export const fetchPolicies = {
@@ -18,6 +19,31 @@ export const fetchPolicies = {
CACHE_ONLY: 'cache-only',
};
+export const stripWhitespaceFromQuery = (url, path) => {
+ /* eslint-disable-next-line no-unused-vars */
+ const [_, params] = url.split(path);
+
+ if (!params) {
+ return url;
+ }
+
+ const decoded = decodeURIComponent(params);
+ const paramsObj = queryToObject(decoded);
+
+ if (!paramsObj.query) {
+ return url;
+ }
+
+ const stripped = paramsObj.query
+ .split(/\s+|\n/)
+ .join(' ')
+ .trim();
+ paramsObj.query = stripped;
+
+ const reassembled = objectToQuery(paramsObj);
+ return `${path}?${reassembled}`;
+};
+
export default (resolvers = {}, config = {}) => {
const {
assumeImmutableResults,
@@ -58,10 +84,31 @@ export default (resolvers = {}, config = {}) => {
});
});
+ /*
+ This custom fetcher intervention is to deal with an issue where we are using GET to access
+ eTag polling, but Apollo Client adds excessive whitespace, which causes the
+ request to fail on certain self-hosted stacks. When we can move
+ to subscriptions entirely or can land an upstream PR, this can be removed.
+
+ Related links
+ Bug report: https://gitlab.com/gitlab-org/gitlab/-/issues/329895
+ Moving to subscriptions: https://gitlab.com/gitlab-org/gitlab/-/issues/332485
+ Apollo Client issue: https://github.com/apollographql/apollo-feature-requests/issues/182
+ */
+
+ const fetchIntervention = (url, options) => {
+ return fetch(stripWhitespaceFromQuery(url, path), options);
+ };
+
+ const requestLink = ApolloLink.split(
+ () => useGet,
+ new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
+ new BatchHttpLink(httpOptions),
+ );
+
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
- useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
);
const performanceBarLink = new ApolloLink((operation, forward) => {
@@ -99,6 +146,7 @@ export default (resolvers = {}, config = {}) => {
new StartupJSLink(),
apolloCaptchaLink,
uploadsLink,
+ requestLink,
]),
);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 8b02d2567a4..7922ff22a70 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -108,25 +108,6 @@ export function getParameterValues(sParam, url = window.location) {
}
/**
- * This function accepts the `name` of the param to parse in the url
- * if the name does not exist this function will return `null`
- * otherwise it will return the value of the param key provided
- *
- * @param {String} name
- * @param {String?} urlToParse
- * @returns value of the parameter as string
- */
-export const getParameterByName = (name, urlToParse) => {
- const url = urlToParse || window.location.href;
- const parsedName = name.replace(/[[\]]/g, '\\$&');
- const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
- const results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeUrlParameter(results[2]);
-};
-
-/**
* Merges a URL to a set of params replacing value for
* those already present.
*
@@ -514,6 +495,19 @@ export function queryToObject(query, { gatherArrays = false, legacySpacesDecode
}
/**
+ * This function accepts the `name` of the param to parse in the url
+ * if the name does not exist this function will return `null`
+ * otherwise it will return the value of the param key provided
+ *
+ * @param {String} name
+ * @param {String?} urlToParse
+ * @returns value of the parameter as string
+ */
+export const getParameterByName = (name, query = window.location.search) => {
+ return queryToObject(query)[name] || null;
+};
+
+/**
* Convert search query object back into a search query
*
* @param {Object?} params that needs to be converted
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index 05b87abecd5..e01f9cc79c0 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -158,6 +158,12 @@ export default {
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
+ this.$emit('updateCommitSha', { newBranch });
+
+ // refetching the content will cause a lot of components to re-render,
+ // including the text editor which uses the commit sha to register the CI schema
+ // so we need to make sure the commit sha is updated first
+ await this.$nextTick();
this.$emit('refetchContent');
},
async setSearchTerm(newSearchTerm) {
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index 368a026bdaa..6af3361e7e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -66,6 +66,7 @@ export default {
},
data() {
return {
+ commitSha: '',
hasError: false,
};
},
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
new file mode 100644
index 00000000000..dce17cad808
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateCommitSha($commitSha: String) {
+ updateCommitSha(commitSha: $commitSha) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
new file mode 100644
index 00000000000..219c23bb22b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
@@ -0,0 +1,12 @@
+query getLatestCommitSha($projectPath: ID!, $ref: String) {
+ project(fullPath: $projectPath) {
+ pipelines(ref: $ref) {
+ nodes {
+ id
+ sha
+ path
+ commitPath
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index ad333f6d42a..2bec2006e95 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,5 +1,6 @@
import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
+import getCommitShaQuery from './queries/client/commit_sha.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
@@ -31,7 +32,15 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
- updateCurrentBranch: (_, { currentBranch = undefined }, { cache }) => {
+ updateCommitSha: (_, { commitSha }, { cache }) => {
+ cache.writeQuery({
+ query: getCommitShaQuery,
+ data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
+ draftData.commitSha = commitSha;
+ }),
+ });
+ },
+ updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
data: produce(cache.readQuery({ query: getCurrentBranchQuery }), (draftData) => {
@@ -39,7 +48,7 @@ export const resolvers = {
}),
});
},
- updateLastCommitBranch: (_, { lastCommitBranch = undefined }, { cache }) => {
+ updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
cache.writeQuery({
query: getLastCommitBranchQuery,
data: produce(cache.readQuery({ query: getLastCommitBranchQuery }), (draftData) => {
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index c7de8516c86..758c8c51a5b 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -16,12 +16,14 @@ import {
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
+import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
@@ -250,6 +252,38 @@ export default {
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
+ async updateCommitSha({ newBranch }) {
+ let fetchResults;
+
+ try {
+ fetchResults = await this.$apollo.query({
+ query: getLatestCommitShaQuery,
+ variables: {
+ projectPath: this.projectFullPath,
+ ref: newBranch,
+ },
+ });
+ } catch {
+ this.showFetchError();
+ return;
+ }
+
+ if (fetchResults.errors?.length > 0) {
+ this.showFetchError();
+ return;
+ }
+
+ const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
+ if (pipelineNodes.length === 0) {
+ return;
+ }
+
+ const commitSha = pipelineNodes[0].sha;
+ this.$apollo.mutate({
+ mutation: updateCommitShaMutation,
+ variables: { commitSha },
+ });
+ },
updateOnCommit({ type }) {
this.reportSuccess(type);
@@ -302,6 +336,7 @@ export default {
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
+ @updateCommitSha="updateCommitSha"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 07d8f3cc5f1..a0129dd536b 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -131,7 +131,8 @@ export default {
<div class="col-lg-8">
<div class="form-group">
<gl-button
- variant="success"
+ category="primary"
+ variant="confirm"
name="commit"
type="submit"
:disabled="!isSubmitEnabled"
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 794a8a85cc5..cf1cff9023e 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -2,7 +2,7 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
import createFlash from '~/flash';
import { __ } from '../../locale';
-import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT } from '../constants';
+import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import { readmeFile } from '../utils/readme';
@@ -36,6 +36,7 @@ export default {
return {
projectPath: '',
nextPageCursor: '',
+ pagesLoaded: 1,
entries: {
trees: [],
submodules: [],
@@ -44,16 +45,26 @@ export default {
isLoadingFiles: false,
isOverLimit: false,
clickedShowMore: false,
- pageSize: TREE_PAGE_SIZE,
fetchCounter: 0,
};
},
computed: {
+ pageSize() {
+ // we want to exponentially increase the page size to reduce the load on the frontend
+ const exponentialSize = (TREE_PAGE_SIZE / TREE_INITIAL_FETCH_COUNT) * (this.fetchCounter + 1);
+ return exponentialSize < TREE_PAGE_SIZE ? exponentialSize : TREE_PAGE_SIZE;
+ },
+ totalEntries() {
+ return Object.values(this.entries).flat().length;
+ },
readme() {
return readmeFile(this.entries.blobs);
},
+ pageLimitReached() {
+ return this.totalEntries / this.pagesLoaded >= TREE_PAGE_LIMIT;
+ },
hasShowMore() {
- return !this.clickedShowMore && this.fetchCounter === TREE_INITIAL_FETCH_COUNT;
+ return !this.clickedShowMore && this.pageLimitReached;
},
},
@@ -104,7 +115,7 @@ export default {
if (pageInfo?.hasNextPage) {
this.nextPageCursor = pageInfo.endCursor;
this.fetchCounter += 1;
- if (this.fetchCounter < TREE_INITIAL_FETCH_COUNT || this.clickedShowMore) {
+ if (!this.pageLimitReached || this.clickedShowMore) {
this.fetchFiles();
this.clickedShowMore = false;
}
@@ -127,6 +138,7 @@ export default {
},
handleShowMore() {
this.clickedShowMore = true;
+ this.pagesLoaded += 1;
this.fetchFiles();
},
},
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 62d5d3db445..22349261d3c 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -1,4 +1,3 @@
-const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
-
+export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 6668af33a1c..82d768c2351 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -240,3 +240,13 @@ $gl-line-height-42: px-to-rem(42px);
}
}
}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1490
+.gl-w-grid-size-28 {
+ width: $grid-size * 28;
+}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1491
+.gl-min-w-8 {
+ min-width: $gl-spacing-scale-8;
+}
diff --git a/app/graphql/resolvers/ci/template_resolver.rb b/app/graphql/resolvers/ci/template_resolver.rb
index dd910116544..7f5a1a486d7 100644
--- a/app/graphql/resolvers/ci/template_resolver.rb
+++ b/app/graphql/resolvers/ci/template_resolver.rb
@@ -6,7 +6,7 @@ module Resolvers
type Types::Ci::TemplateType, null: true
argument :name, GraphQL::STRING_TYPE, required: true,
- description: 'Name of the CI/CD template to search for.'
+ description: 'Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`.'
alias_method :project, :object
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 23f2a082a68..882302f05ad 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -9,7 +9,6 @@ module Ci
"artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'),
"deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'),
"runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'),
- "variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'),
"page_path" => project_job_path(@project, @build),
"build_status" => @build.status,
"build_stage" => @build.stage,
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index 1a30b6bed08..d441ffbb853 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -9,7 +9,9 @@ module Ci
end
def js_pipeline_editor_data(project)
- commit_sha = project.commit ? project.commit.sha : ''
+ initial_branch = params[:branch_name]
+ latest_commit = project.repository.commit(initial_branch) || project.commit
+ commit_sha = latest_commit ? latest_commit.sha : ''
{
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
@@ -17,11 +19,11 @@ module Ci
"commit-sha" => commit_sha,
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
- "initial-branch-name": params[:branch_name],
+ "initial-branch-name" => initial_branch,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
- "pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
+ "pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
"pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 7679f6fce72..dc37d73df85 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -27,6 +27,9 @@ class AwardEmoji < ApplicationRecord
after_save :expire_cache
after_destroy :expire_cache
+ after_save :update_awardable_upvotes_count
+ after_destroy :update_awardable_upvotes_count
+
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count')
@@ -64,6 +67,14 @@ class AwardEmoji < ApplicationRecord
awardable.try(:bump_updated_at)
awardable.try(:expire_etag_cache)
end
+
+ private
+
+ def update_awardable_upvotes_count
+ return unless upvote? && awardable.has_attribute?(:upvotes_count)
+
+ awardable.update_column(:upvotes_count, awardable.upvotes)
+ end
end
AwardEmoji.prepend_mod_with('AwardEmoji')
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 90f63f82a66..ed14a1bce41 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ErrorTracking::ErrorEvent < ApplicationRecord
- belongs_to :error
+ belongs_to :error, counter_cache: :events_count
validates :payload, json_schema: { filename: 'error_tracking_event_payload' }
diff --git a/app/validators/json_schemas/error_tracking_event_payload.json b/app/validators/json_schemas/error_tracking_event_payload.json
index 79e81672d0e..19abde7de08 100644
--- a/app/validators/json_schemas/error_tracking_event_payload.json
+++ b/app/validators/json_schemas/error_tracking_event_payload.json
@@ -2,6 +2,9 @@
"description": "Error tracking event payload",
"type": "object",
"required": [],
+ "modules": {
+ "type": "object"
+ },
"properties": {
"event_id": {
"type": "string"
@@ -73,28 +76,7 @@
}
},
"trace": {
- "type": "object",
- "required": [],
- "properties": {
- "trace_id": {
- "type": "string"
- },
- "span_id": {
- "type": "string"
- },
- "parent_span_id": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "op": {
- "type": "string"
- },
- "status": {
- "type": "string"
- }
- }
+ "type": "object"
}
}
},
@@ -118,52 +100,13 @@
"type": "string"
},
"data": {
- "type": "object",
- "required": [],
- "properties": {
- "controller": {
- "type": "string"
- },
- "action": {
- "type": "string"
- },
- "params": {
- "type": "object",
- "required": [],
- "properties": {
- "controller": {
- "type": "string"
- },
- "action": {
- "type": "string"
- }
- }
- },
- "format": {
- "type": "string"
- },
- "method": {
- "type": "string"
- },
- "path": {
- "type": "string"
- },
- "start_timestamp": {
- "type": "number"
- }
- }
- },
- "level": {
- "type": "string"
+ "type": "object"
},
"message": {
"type": "string"
},
"timestamp": {
"type": "number"
- },
- "type": {
- "type": "string"
}
}
}
@@ -199,37 +142,7 @@
"type": "string"
},
"headers": {
- "type": "object",
- "required": [],
- "properties": {
- "Host": {
- "type": "string"
- },
- "User-Agent": {
- "type": "string"
- },
- "Accept": {
- "type": "string"
- },
- "Accept-Language": {
- "type": "string"
- },
- "Accept-Encoding": {
- "type": "string"
- },
- "Referer": {
- "type": "string"
- },
- "Turbolinks-Referrer": {
- "type": "string"
- },
- "Connection": {
- "type": "string"
- },
- "X-Request-Id": {
- "type": "string"
- }
- }
+ "type": "object"
},
"env": {
"type": "object",
@@ -290,25 +203,19 @@
"type": "number"
},
"in_app": {
- "type": "string"
+ "type": "boolean"
},
"filename": {
"type": "string"
},
"pre_context": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "type": "array"
},
"context_line": {
"type": "string"
},
"post_context": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "type": "array"
}
}
}
diff --git a/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml b/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml
index 66597f95e8c..81200aff786 100644
--- a/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml
+++ b/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334264
milestone: '14.1'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/api_caching_repository_compare.yml b/config/feature_flags/development/api_caching_repository_compare.yml
deleted file mode 100644
index d39bd283512..00000000000
--- a/config/feature_flags/development/api_caching_repository_compare.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: api_caching_repository_compare
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64418
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334264
-milestone: '14.1'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/danger/gitaly/Dangerfile b/danger/gitaly/Dangerfile
new file mode 100644
index 00000000000..59e55845c83
--- /dev/null
+++ b/danger/gitaly/Dangerfile
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+TEMPLATE_MESSAGE = <<~MSG
+This merge request requires coordination with gitaly deployments.
+Before merging this merge request we should verify that gitaly
+running in production already implements the new gRPC interface
+included here.
+
+Failing to do so will introduce a [non backward compatible
+change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html)
+during canary depoyment that can cause an incident.
+
+1. Identify the gitaly MR introducing the new interface
+1. Verify that the environment widget contains a `gprd` deployment
+MSG
+
+changed_lines = helper.changed_lines('Gemfile.lock')
+if changed_lines.any? { |line| line =~ /^\+\s+gitaly \(/ }
+ warn 'Changing gitaly gem can cause a multi-version incompatibility incident'
+
+ markdown(TEMPLATE_MESSAGE)
+end
diff --git a/db/migrate/20210701111627_add_upvotes_count_to_issues.rb b/db/migrate/20210701111627_add_upvotes_count_to_issues.rb
new file mode 100644
index 00000000000..beefb186f37
--- /dev/null
+++ b/db/migrate/20210701111627_add_upvotes_count_to_issues.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUpvotesCountToIssues < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ with_lock_retries do
+ add_column :issues, :upvotes_count, :integer, default: 0, null: false
+ end
+ end
+
+ def down
+ remove_column :issues, :upvotes_count
+ end
+end
diff --git a/db/migrate/20210708131048_add_error_tracking_counter_cache.rb b/db/migrate/20210708131048_add_error_tracking_counter_cache.rb
new file mode 100644
index 00000000000..3bf7e1e3688
--- /dev/null
+++ b/db/migrate/20210708131048_add_error_tracking_counter_cache.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddErrorTrackingCounterCache < ActiveRecord::Migration[6.1]
+ def up
+ add_column :error_tracking_errors, :events_count, :bigint, null: false, default: 0
+ end
+
+ def down
+ remove_column :error_tracking_errors, :events_count
+ end
+end
diff --git a/db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb b/db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb
new file mode 100644
index 00000000000..0afc0bc1d08
--- /dev/null
+++ b/db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillIssuesUpvotesCount < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ MIGRATION = 'BackfillUpvotesCountOnIssues'
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 5_000
+
+ def up
+ scope = Issue.joins("INNER JOIN award_emoji e ON e.awardable_id = issues.id AND e.awardable_type = 'Issue' AND e.name = 'thumbsup'")
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ scope,
+ MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE
+ )
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20210701111627 b/db/schema_migrations/20210701111627
new file mode 100644
index 00000000000..ca52a786a22
--- /dev/null
+++ b/db/schema_migrations/20210701111627
@@ -0,0 +1 @@
+c2efdad12c3d0ec5371259baa91466137b827f513250e901842ab28e56c3de0a \ No newline at end of file
diff --git a/db/schema_migrations/20210701111909 b/db/schema_migrations/20210701111909
new file mode 100644
index 00000000000..ed6e2d56e8d
--- /dev/null
+++ b/db/schema_migrations/20210701111909
@@ -0,0 +1 @@
+fdd7509fc88e563b65b487706cae1a64066a7ba7d4bd13d0414b8431c3ddfb68 \ No newline at end of file
diff --git a/db/schema_migrations/20210708131048 b/db/schema_migrations/20210708131048
new file mode 100644
index 00000000000..f61978d8e0f
--- /dev/null
+++ b/db/schema_migrations/20210708131048
@@ -0,0 +1 @@
+ed0c0dc015e7c3457248303b8b478c8d259d6a800a2bfed8b05b1f976b6794a7 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b21c1bfd484..6b583c8da46 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12790,6 +12790,7 @@ CREATE TABLE error_tracking_errors (
platform text,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
+ events_count bigint DEFAULT 0 NOT NULL,
CONSTRAINT check_18a758e537 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_b5cb4d3888 CHECK ((char_length(actor) <= 255)),
CONSTRAINT check_c739788b12 CHECK ((char_length(description) <= 1024)),
@@ -14267,6 +14268,7 @@ CREATE TABLE issues (
sprint_id bigint,
issue_type smallint DEFAULT 0 NOT NULL,
blocking_issues_count integer DEFAULT 0 NOT NULL,
+ upvotes_count integer DEFAULT 0 NOT NULL,
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 7fadc6e5cce..cbf92fce85d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11596,7 +11596,7 @@ Returns [`CiTemplate`](#citemplate).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectcitemplatename"></a>`name` | [`String!`](#string) | Name of the CI/CD template to search for. |
+| <a id="projectcitemplatename"></a>`name` | [`String!`](#string) | Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`. |
##### `Project.clusterAgent`
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index cb102823057..45d516c4f49 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -49,8 +49,10 @@ The following table lists project permissions available for each role:
| View allowed and denied licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
-| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
-| View License list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
+| View License list **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
+| View [Threats list](application_security/threat_monitoring/#threat-monitoring) **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
+| Create and run [on-demand DAST scans](application_security/dast/#on-demand-scans) | | | ✓ | ✓ | ✓ |
| View licenses in Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 66f56f0b984..f274406e225 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -122,7 +122,7 @@ module API
get ':id/repository/compare' do
ff_enabled = Feature.enabled?(:api_caching_rate_limit_repository_compare, user_project, default_enabled: :yaml)
- cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 30.seconds) do
+ cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 1.minute) do
if params[:from_project_id].present?
target_project = MergeRequestTargetProjectFinder
.new(current_user: current_user, source_project: user_project, project_feature: :repository)
@@ -138,11 +138,7 @@ module API
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
if compare
- if Feature.enabled?(:api_caching_repository_compare, user_project, default_enabled: :yaml)
- present_cached compare, with: Entities::Compare, expires_in: 1.day, cache_context: nil
- else
- present compare, with: Entities::Compare
- end
+ present compare, with: Entities::Compare
else
not_found!("Ref")
end
diff --git a/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb b/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb
new file mode 100644
index 00000000000..170af90805a
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Class that will populate the upvotes_count field
+ # for each issue
+ class BackfillUpvotesCountOnIssues
+ BATCH_SIZE = 1_000
+
+ def perform(start_id, stop_id)
+ (start_id..stop_id).step(BATCH_SIZE).each do |offset|
+ update_issue_upvotes_count(offset, offset + BATCH_SIZE)
+ end
+ end
+
+ private
+
+ def execute(sql)
+ @connection ||= ::ActiveRecord::Base.connection
+ @connection.execute(sql)
+ end
+
+ def update_issue_upvotes_count(batch_start, batch_stop)
+ execute(<<~SQL)
+ UPDATE issues
+ SET upvotes_count = sub_q.count_all
+ FROM (
+ SELECT COUNT(*) AS count_all, e.awardable_id AS issue_id
+ FROM award_emoji AS e
+ WHERE e.name = 'thumbsup' AND
+ e.awardable_type = 'Issue' AND
+ e.awardable_id BETWEEN #{batch_start} AND #{batch_stop}
+ GROUP BY issue_id
+ ) AS sub_q
+ WHERE sub_q.issue_id = issues.id;
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 4e2c5d033ac..a84978a2a80 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -229,6 +229,7 @@ excluded_attributes:
- :promoted_to_epic_id
- :blocking_issues_count
- :service_desk_reply_to
+ - :upvotes_count
merge_request:
- :milestone_id
- :sprint_id
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5c259a6fb70..0c23d0efbf6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16865,6 +16865,9 @@ msgstr ""
msgid "InProductMarketing|Create your first project!"
msgstr ""
+msgid "InProductMarketing|Deliver Better Products Faster"
+msgstr ""
+
msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?"
msgstr ""
@@ -16904,6 +16907,9 @@ msgstr ""
msgid "InProductMarketing|Follow our steps"
msgstr ""
+msgid "InProductMarketing|Free 30-day trial"
+msgstr ""
+
msgid "InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy."
msgstr ""
@@ -16982,6 +16988,9 @@ msgstr ""
msgid "InProductMarketing|Improve code quality and streamline reviews"
msgstr ""
+msgid "InProductMarketing|Increase Operational Efficiencies"
+msgstr ""
+
msgid "InProductMarketing|Invite your colleagues and start shipping code faster."
msgstr ""
@@ -17024,12 +17033,18 @@ msgstr ""
msgid "InProductMarketing|Neutral"
msgstr ""
+msgid "InProductMarketing|No credit card required."
+msgstr ""
+
msgid "InProductMarketing|Our tool brings all the things together"
msgstr ""
msgid "InProductMarketing|Rapid development, simplified"
msgstr ""
+msgid "InProductMarketing|Reduce Security & Compliance Risk"
+msgstr ""
+
msgid "InProductMarketing|Security that's integrated into your development lifecycle"
msgstr ""
@@ -17120,6 +17135,9 @@ msgstr ""
msgid "InProductMarketing|Use GitLab CI/CD"
msgstr ""
+msgid "InProductMarketing|Used by more than 100,000 organizations from around the globe:"
+msgstr ""
+
msgid "InProductMarketing|Very difficult"
msgstr ""
@@ -29087,6 +29105,9 @@ msgstr ""
msgid "SecurityOrchestration|Security policy project"
msgstr ""
+msgid "SecurityPolicies|All policies"
+msgstr ""
+
msgid "SecurityPolicies|Description"
msgstr ""
diff --git a/package.json b/package.json
index 1d839a200f4..fbe6424c741 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.202.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "31.0.1",
+ "@gitlab/ui": "31.2.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.3-2",
"@rails/ujs": "6.1.3-2",
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
index cce38456df9..696a7f4ee8a 100644
--- a/spec/features/projects/user_views_empty_project_spec.rb
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -7,10 +7,12 @@ RSpec.describe 'User views an empty project' do
let_it_be(:user) { create(:user) }
shared_examples 'allowing push to default branch' do
- it 'shows push-to-master instructions' do
+ let(:default_branch) { project.default_branch_or_main }
+
+ it 'shows push-to-default-branch instructions' do
visit project_path(project)
- expect(page).to have_content('git push -u origin master')
+ expect(page).to have_content("git push -u origin #{default_branch}")
end
end
@@ -47,7 +49,7 @@ RSpec.describe 'User views an empty project' do
it 'does not show push-to-master instructions' do
visit project_path(project)
- expect(page).not_to have_content('git push -u origin master')
+ expect(page).not_to have_content('git push -u origin')
end
end
end
@@ -61,7 +63,7 @@ RSpec.describe 'User views an empty project' do
it 'does not show push-to-master instructions nor invite members link', :aggregate_failures, :js do
visit project_path(project)
- expect(page).not_to have_content('git push -u origin master')
+ expect(page).not_to have_content('git push -u origin')
expect(page).not_to have_button(text: 'Invite members')
end
end
diff --git a/spec/frontend/content_editor/services/upload_file_spec.js b/spec/frontend/content_editor/services/upload_file_spec.js
new file mode 100644
index 00000000000..87c5298079e
--- /dev/null
+++ b/spec/frontend/content_editor/services/upload_file_spec.js
@@ -0,0 +1,46 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { uploadFile } from '~/content_editor/services/upload_file';
+import httpStatus from '~/lib/utils/http_status';
+
+describe('content_editor/services/upload_file', () => {
+ const uploadsPath = '/uploads';
+ const file = new File(['content'], 'file.txt');
+ // TODO: Replace with automated fixture
+ const renderedAttachmentLinkFixture =
+ '<a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"><img data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"></a></p>';
+ const successResponse = {
+ link: {
+ markdown: '[GitLab](https://gitlab.com)',
+ },
+ };
+ const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
+ let mock;
+ let renderMarkdown;
+ let renderedMarkdown;
+
+ beforeEach(() => {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ renderedMarkdown = parseHTML(renderedAttachmentLinkFixture);
+
+ mock = new MockAdapter(axios);
+ mock.onPost(uploadsPath, formData).reply(httpStatus.OK, successResponse);
+ renderMarkdown = jest.fn().mockResolvedValue(renderedAttachmentLinkFixture);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('returns src and canonicalSrc of uploaded file', async () => {
+ const response = await uploadFile({ uploadsPath, renderMarkdown, file });
+
+ expect(renderMarkdown).toHaveBeenCalledWith(successResponse.link.markdown);
+ expect(response).toEqual({
+ src: renderedMarkdown.querySelector('a').getAttribute('href'),
+ canonicalSrc: renderedMarkdown.querySelector('a').dataset.canonicalSrc,
+ });
+ });
+});
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
index dbef547c297..99f13a1c84c 100644
--- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -54,7 +54,7 @@ describe('Compare diff version dropdowns', () => {
Object.defineProperty(window, 'location', {
writable: true,
- value: { href: `https://example.gitlab.com${diffHeadParam}` },
+ value: { search: diffHeadParam },
});
expectedFirstVersion = {
diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index c9de110ce06..9738fd14275 100644
--- a/spec/frontend/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
@@ -9,7 +9,6 @@ describe('Empty State', () => {
illustrationSizeClass: 'svg-430',
title: 'This job has not started yet',
playable: false,
- variablesSettingsUrl: '',
};
const createWrapper = (props) => {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 3fcefde1aba..871969205b7 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -37,7 +37,6 @@ describe('Job App', () => {
deploymentHelpUrl: 'help/deployment',
codeQualityHelpPath: '/help/code_quality',
runnerSettingsUrl: 'settings/ci-cd/runners',
- variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
projectPath: 'user-name/project-name',
subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes',
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 376a822dde5..7e42ee957d3 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,3 +1,4 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -18,7 +19,6 @@ describe('Manual Variables Form', () => {
method: 'post',
button_title: 'Trigger this manual action',
},
- variablesSettingsUrl: '/settings',
};
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
@@ -33,15 +33,19 @@ describe('Manual Variables Form', () => {
propsData: { ...requiredProps, ...props },
localVue,
store,
+ stubs: {
+ GlSprintf,
+ },
}),
);
};
const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
+ const findHelpText = () => wrapper.findComponent(GlSprintf);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
- const findHelpText = () => wrapper.findByTestId('form-help-text');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
@@ -62,11 +66,10 @@ describe('Manual Variables Form', () => {
});
it('renders help text with provided link', () => {
- expect(findHelpText().text()).toBe(
- 'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default',
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
-
- expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl);
});
describe('when adding a new variable', () => {
diff --git a/spec/frontend/lib/graphql_spec.js b/spec/frontend/lib/graphql_spec.js
new file mode 100644
index 00000000000..a39ce2ffd99
--- /dev/null
+++ b/spec/frontend/lib/graphql_spec.js
@@ -0,0 +1,54 @@
+import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { stripWhitespaceFromQuery } from '~/lib/graphql';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+describe('stripWhitespaceFromQuery', () => {
+ const operationName = 'getPipelineDetails';
+ const variables = `{
+ projectPath: 'root/abcd-dag',
+ iid: '44'
+ }`;
+
+ const testQuery = getPipelineDetails.loc.source.body;
+ const defaultPath = '/api/graphql';
+ const encodedVariables = encodeURIComponent(variables);
+
+ it('shortens the query argument by replacing multiple spaces and newlines with a single space', () => {
+ const testString = `${defaultPath}?query=${encodeURIComponent(testQuery)}`;
+ expect(testString.length > stripWhitespaceFromQuery(testString, defaultPath).length).toBe(true);
+ });
+
+ it('does not contract a single space', () => {
+ const simpleSingleString = `${defaultPath}?query=${encodeURIComponent('fragment Nonsense')}`;
+ expect(stripWhitespaceFromQuery(simpleSingleString, defaultPath)).toEqual(simpleSingleString);
+ });
+
+ it('works with a non-default path', () => {
+ const newPath = 'another/graphql/path';
+ const newPathSingleString = `${newPath}?query=${encodeURIComponent('fragment Nonsense')}`;
+ expect(stripWhitespaceFromQuery(newPathSingleString, newPath)).toEqual(newPathSingleString);
+ });
+
+ it('does not alter other arguments', () => {
+ const bareParams = `?query=${encodeURIComponent(
+ testQuery,
+ )}&operationName=${operationName}&variables=${encodedVariables}`;
+ const testLongString = `${defaultPath}${bareParams}`;
+
+ const processed = stripWhitespaceFromQuery(testLongString, defaultPath);
+ const decoded = decodeURIComponent(processed);
+ const params = queryToObject(decoded);
+
+ expect(params.operationName).toBe(operationName);
+ expect(params.variables).toBe(variables);
+ });
+
+ it('works when there are no query params', () => {
+ expect(stripWhitespaceFromQuery(defaultPath, defaultPath)).toEqual(defaultPath);
+ });
+
+ it('works when the params do not include a query', () => {
+ const paramsWithoutQuery = `${defaultPath}&variables=${encodedVariables}`;
+ expect(stripWhitespaceFromQuery(paramsWithoutQuery, defaultPath)).toEqual(paramsWithoutQuery);
+ });
+});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 1a9711210a0..66d0faa95e7 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -101,48 +101,6 @@ describe('URL utility', () => {
});
});
- describe('getParameterByName', () => {
- const { getParameterByName } = urlUtils;
-
- it('should return valid parameter', () => {
- setWindowLocation({ href: 'https://gitlab.com?scope=all&p=2' });
-
- expect(getParameterByName('p')).toEqual('2');
- expect(getParameterByName('scope')).toBe('all');
- });
-
- it('should return invalid parameter', () => {
- setWindowLocation({ href: 'https://gitlab.com?scope=all&p=2' });
-
- expect(getParameterByName('fakeParameter')).toBe(null);
- });
-
- it('should return a parameter with spaces', () => {
- setWindowLocation({ href: 'https://gitlab.com?search=my terms' });
-
- expect(getParameterByName('search')).toBe('my terms');
- });
-
- it('should return a parameter with encoded spaces', () => {
- setWindowLocation({ href: 'https://gitlab.com?search=my%20terms' });
-
- expect(getParameterByName('search')).toBe('my terms');
- });
-
- it('should return a parameter with plus signs as spaces', () => {
- setWindowLocation({ href: 'https://gitlab.com?search=my+terms' });
-
- expect(getParameterByName('search')).toBe('my terms');
- });
-
- it('should return valid parameters if URL is provided', () => {
- expect(getParameterByName('foo', 'http://cocteau.twins?foo=bar')).toBe('bar');
- expect(getParameterByName('manan', 'http://cocteau.twins?foo=bar&manan=canchu')).toBe(
- 'canchu',
- );
- });
- });
-
describe('mergeUrlParams', () => {
const { mergeUrlParams } = urlUtils;
@@ -762,6 +720,49 @@ describe('URL utility', () => {
});
});
+ describe('getParameterByName', () => {
+ const { getParameterByName } = urlUtils;
+
+ it('should return valid parameter', () => {
+ setWindowLocation({ search: '?scope=all&p=2' });
+
+ expect(getParameterByName('p')).toEqual('2');
+ expect(getParameterByName('scope')).toBe('all');
+ });
+
+ it('should return invalid parameter', () => {
+ setWindowLocation({ search: '?scope=all&p=2' });
+
+ expect(getParameterByName('fakeParameter')).toBe(null);
+ });
+
+ it('should return a parameter with spaces', () => {
+ setWindowLocation({ search: '?search=my terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return a parameter with encoded spaces', () => {
+ setWindowLocation({ search: '?search=my%20terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return a parameter with plus signs as spaces', () => {
+ setWindowLocation({ search: '?search=my+terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return valid parameters if search is provided', () => {
+ expect(getParameterByName('foo', 'foo=bar')).toBe('bar');
+ expect(getParameterByName('foo', '?foo=bar')).toBe('bar');
+
+ expect(getParameterByName('manan', 'foo=bar&manan=canchu')).toBe('canchu');
+ expect(getParameterByName('manan', '?foo=bar&manan=canchu')).toBe('canchu');
+ });
+ });
+
describe('objectToQuery', () => {
it('converts search query object back into a search query', () => {
const searchQueryObject = { one: '1', two: '2' };
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index e731ad8695e..85b51d08f88 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -207,7 +207,8 @@ describe('Pipeline editor branch switcher', () => {
it('updates session history when selecting a different branch', async () => {
const branch = findDropdownItems().at(1);
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(window.history.pushState).toHaveBeenCalled();
expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`);
@@ -215,7 +216,8 @@ describe('Pipeline editor branch switcher', () => {
it('does not update session history when selecting current branch', async () => {
const branch = findDropdownItems().at(0);
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(branch.text()).toBe(mockDefaultBranch);
expect(window.history.pushState).not.toHaveBeenCalled();
@@ -227,7 +229,8 @@ describe('Pipeline editor branch switcher', () => {
expect(branch.text()).not.toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeDefined();
expect(wrapper.emitted('refetchContent')).toHaveLength(1);
@@ -239,10 +242,20 @@ describe('Pipeline editor branch switcher', () => {
expect(branch.text()).toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
+
+ it('emits the updateCommitSha event when selecting a different branch', async () => {
+ expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
+
+ const branch = findDropdownItems().at(1);
+ branch.vm.$emit('click');
+
+ expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
+ });
});
describe('when searching', () => {
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 4b0f1aaa13c..4d4a8c21d78 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -156,6 +156,35 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
+export const mockNewCommitShaResults = {
+ data: {
+ project: {
+ pipelines: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/1',
+ sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
+ path: `/${mockProjectFullPath}/-/pipelines/488`,
+ commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/2',
+ sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
+ path: `/${mockProjectFullPath}/-/pipelines/487`,
+ commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/3',
+ sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
+ path: `/${mockProjectFullPath}/-/pipelines/433`,
+ commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
+ },
+ ],
+ },
+ },
+ },
+};
+
export const mockProjectBranches = {
data: {
project: {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index eaf7d5e36a1..3dd09c583d6 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -12,7 +12,9 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import {
@@ -24,6 +26,7 @@ import {
mockDefaultBranch,
mockProjectFullPath,
mockCiYml,
+ mockNewCommitShaResults,
} from './mock_data';
const localVue = createLocalVue();
@@ -49,6 +52,9 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData;
let mockCiConfigData;
let mockGetTemplate;
+ let mockUpdateCommitSha;
+ let mockLatestCommitShaQuery;
+ let mockPipelineQuery;
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
@@ -84,9 +90,16 @@ describe('Pipeline editor app component', () => {
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
[getTemplate, mockGetTemplate],
+ [getLatestCommitShaQuery, mockLatestCommitShaQuery],
+ [getPipelineQuery, mockPipelineQuery],
];
- mockApollo = createMockApollo(handlers);
+ const resolvers = {
+ Mutation: {
+ updateCommitSha: mockUpdateCommitSha,
+ },
+ };
+ mockApollo = createMockApollo(handlers, resolvers);
const options = {
localVue,
@@ -116,6 +129,9 @@ describe('Pipeline editor app component', () => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
mockGetTemplate = jest.fn();
+ mockUpdateCommitSha = jest.fn();
+ mockLatestCommitShaQuery = jest.fn();
+ mockPipelineQuery = jest.fn();
});
afterEach(() => {
@@ -347,4 +363,45 @@ describe('Pipeline editor app component', () => {
expect(findTextEditor().exists()).toBe(true);
});
});
+
+ describe('when updating commit sha', () => {
+ const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
+
+ beforeEach(async () => {
+ mockUpdateCommitSha.mockResolvedValue(newCommitSha);
+ mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
+ await createComponentWithApollo();
+ });
+
+ it('fetches updated commit sha for the new branch', async () => {
+ expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
+
+ wrapper
+ .findComponent(PipelineEditorHome)
+ .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
+ await waitForPromises();
+
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
+ projectPath: mockProjectFullPath,
+ ref: 'new-branch',
+ });
+ });
+
+ it('updates commit sha with the newly fetched commit sha', async () => {
+ expect(mockUpdateCommitSha).not.toHaveBeenCalled();
+
+ wrapper
+ .findComponent(PipelineEditorHome)
+ .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
+ await waitForPromises();
+
+ expect(mockUpdateCommitSha).toHaveBeenCalled();
+ expect(mockUpdateCommitSha).toHaveBeenCalledWith(
+ expect.any(Object),
+ { commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+ });
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index d397bc185e2..96c19776513 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import filesQuery from 'shared_queries/repository/files.query.graphql';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from '~/repository/components/tree_content.vue';
-import { TREE_INITIAL_FETCH_COUNT } from '~/repository/constants';
let vm;
let $apollo;
@@ -23,6 +23,8 @@ function factory(path, data = () => ({})) {
}
describe('Repository table component', () => {
+ const findFileTable = () => vm.find(FileTable);
+
afterEach(() => {
vm.destroy();
});
@@ -85,14 +87,12 @@ describe('Repository table component', () => {
describe('FileTable showMore', () => {
describe('when is present', () => {
- const fileTable = () => vm.find(FileTable);
-
beforeEach(async () => {
factory('/');
});
it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
await vm.vm.$nextTick();
@@ -100,7 +100,7 @@ describe('Repository table component', () => {
});
it('changes clickedShowMore when "showMore" event is emitted', async () => {
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
await vm.vm.$nextTick();
@@ -110,7 +110,7 @@ describe('Repository table component', () => {
it('triggers fetchFiles when "showMore" event is emitted', () => {
jest.spyOn(vm.vm, 'fetchFiles');
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
expect(vm.vm.fetchFiles).toHaveBeenCalled();
});
@@ -126,10 +126,52 @@ describe('Repository table component', () => {
expect(vm.vm.hasShowMore).toBe(false);
});
- it('has limit of 1000 files on initial load', () => {
+ it.each`
+ totalBlobs | pagesLoaded | limitReached
+ ${900} | ${1} | ${false}
+ ${1000} | ${1} | ${true}
+ ${1002} | ${1} | ${true}
+ ${1002} | ${2} | ${false}
+ ${1900} | ${2} | ${false}
+ ${2000} | ${2} | ${true}
+ `('has limit of 1000 entries per page', async ({ totalBlobs, pagesLoaded, limitReached }) => {
factory('/');
- expect(TREE_INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
+ const blobs = new Array(totalBlobs).fill('fakeBlob');
+ vm.setData({ entries: { blobs }, pagesLoaded });
+
+ await vm.vm.$nextTick();
+
+ expect(findFileTable().props('hasMore')).toBe(limitReached);
+ });
+
+ it.each`
+ fetchCounter | pageSize
+ ${0} | ${10}
+ ${2} | ${30}
+ ${4} | ${50}
+ ${6} | ${70}
+ ${8} | ${90}
+ ${10} | ${100}
+ ${20} | ${100}
+ ${100} | ${100}
+ ${200} | ${100}
+ `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
+ factory('/');
+ vm.setData({ fetchCounter });
+
+ vm.vm.fetchFiles();
+
+ expect($apollo.query).toHaveBeenCalledWith({
+ query: filesQuery,
+ variables: {
+ pageSize,
+ nextPageCursor: '',
+ path: '/',
+ projectPath: '',
+ ref: '',
+ },
+ });
});
});
});
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index feb654a091a..3ce4657282e 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"commit-sha" => project.commit.sha,
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'foo',
- "initial-branch-name": nil,
+ "initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
@@ -72,7 +72,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"commit-sha" => '',
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'foo',
- "initial-branch-name": nil,
+ "initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
@@ -87,5 +87,21 @@ RSpec.describe Ci::PipelineEditorHelper do
})
end
end
+
+ context 'with a non-default branch name' do
+ let(:user) { create(:user) }
+
+ before do
+ create_commit('Message', project, user, 'feature')
+ controller.params[:branch_name] = 'feature'
+ end
+
+ it 'returns correct values' do
+ latest_feature_sha = project.repository.commit('feature').sha
+
+ expect(pipeline_editor_data['initial-branch-name']).to eq('feature')
+ expect(pipeline_editor_data['commit-sha']).to eq(latest_feature_sha)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb
new file mode 100644
index 00000000000..b084e3fe885
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillUpvotesCountOnIssues, schema: 20210701111909 do
+ let(:award_emoji) { table(:award_emoji) }
+
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project1) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:project2) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:issue1) { table(:issues).create!(project_id: project1.id) }
+ let!(:issue2) { table(:issues).create!(project_id: project2.id) }
+ let!(:issue3) { table(:issues).create!(project_id: project2.id) }
+ let!(:issue4) { table(:issues).create!(project_id: project2.id) }
+
+ describe '#perform' do
+ before do
+ add_upvotes(issue1, :thumbsdown, 1)
+ add_upvotes(issue2, :thumbsup, 2)
+ add_upvotes(issue2, :thumbsdown, 1)
+ add_upvotes(issue3, :thumbsup, 3)
+ add_upvotes(issue4, :thumbsup, 4)
+ end
+
+ it 'updates upvotes_count' do
+ subject.perform(issue1.id, issue4.id)
+
+ expect(issue1.reload.upvotes_count).to eq(0)
+ expect(issue2.reload.upvotes_count).to eq(2)
+ expect(issue3.reload.upvotes_count).to eq(3)
+ expect(issue4.reload.upvotes_count).to eq(4)
+ end
+ end
+
+ private
+
+ def add_upvotes(issue, name, count)
+ count.times do
+ award_emoji.create!(
+ name: name.to_s,
+ awardable_type: 'Issue',
+ awardable_id: issue.id
+ )
+ end
+ end
+end
diff --git a/spec/migrations/backfill_issues_upvotes_count_spec.rb b/spec/migrations/backfill_issues_upvotes_count_spec.rb
new file mode 100644
index 00000000000..f2bea0edea0
--- /dev/null
+++ b/spec/migrations/backfill_issues_upvotes_count_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillIssuesUpvotesCount do
+ let(:migration) { described_class.new }
+ let(:issues) { table(:issues) }
+ let(:award_emoji) { table(:award_emoji) }
+
+ let!(:issue1) { issues.create! }
+ let!(:issue2) { issues.create! }
+ let!(:issue3) { issues.create! }
+ let!(:issue4) { issues.create! }
+ let!(:issue4_without_thumbsup) { issues.create! }
+
+ let!(:award_emoji1) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue1.id) }
+ let!(:award_emoji2) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue2.id) }
+ let!(:award_emoji3) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue3.id) }
+ let!(:award_emoji4) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue4.id) }
+
+ it 'correctly schedules background migrations' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(issue1.id, issue2.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(issue3.id, issue4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 484e1a9e266..ebd1441f901 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -171,4 +171,43 @@ RSpec.describe AwardEmoji do
expect(awards).to eq('thumbsup' => 2)
end
end
+
+ describe 'updating upvotes_count' do
+ context 'on an issue' do
+ let(:issue) { create(:issue) }
+ let(:upvote) { build(:award_emoji, :upvote, user: build(:user), awardable: issue) }
+ let(:downvote) { build(:award_emoji, :downvote, user: build(:user), awardable: issue) }
+
+ it 'updates upvotes_count on the issue when saved' do
+ expect(issue).to receive(:update_column).with(:upvotes_count, 1).once
+
+ upvote.save!
+ downvote.save!
+ end
+
+ it 'updates upvotes_count on the issue when destroyed' do
+ expect(issue).to receive(:update_column).with(:upvotes_count, 0).once
+
+ upvote.destroy!
+ downvote.destroy!
+ end
+ end
+
+ context 'on another awardable' do
+ let(:merge_request) { create(:merge_request) }
+ let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: merge_request) }
+
+ it 'does not update upvotes_count on the merge_request when saved' do
+ expect(merge_request).not_to receive(:update_column)
+
+ award_emoji.save!
+ end
+
+ it 'does not update upvotes_count on the merge_request when destroyed' do
+ expect(merge_request).not_to receive(:update_column)
+
+ award_emoji.destroy!
+ end
+ end
+ end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 17cdb5166db..d019e89e0b4 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -488,17 +488,6 @@ RSpec.describe API::Repositories do
let(:current_user) { nil }
end
end
-
- context 'api_caching_repository_compare is disabled' do
- before do
- stub_feature_flags(api_caching_repository_compare: false)
- end
-
- it_behaves_like 'repository compare' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
- end
end
describe 'GET /projects/:id/repository/contributors' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 64fde3db19f..ec34dc7e7a1 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -611,11 +611,12 @@ RSpec.describe API::Wikis do
let(:payload) { { file: fixture_file_upload('spec/fixtures/dk.png') } }
let(:url) { "/projects/#{project.id}/wikis/attachments" }
let(:file_path) { "#{Wikis::CreateAttachmentService::ATTACHMENT_PATH}/fixed_hex/dk.png" }
+ let(:branch) { wiki.default_branch }
let(:result_hash) do
{
file_name: 'dk.png',
file_path: file_path,
- branch: 'master',
+ branch: branch,
link: {
url: file_path,
markdown: "![dk](#{file_path})"
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index d2d706fad3f..9c4e9407f90 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -221,7 +221,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
- expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, gitaly, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
end
end
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index 5e2970169f6..45a53ac2922 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -10,6 +10,7 @@ module Tooling
documentation
duplicate_yarn_dependencies
eslint
+ gitaly
karma
pajamas
pipeline
diff --git a/yarn.lock b/yarn.lock
index 1fda92cad95..a7421c05d8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -908,10 +908,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@31.0.1":
- version "31.0.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.0.1.tgz#55c481f2e2fa777ff34237a8229f39553428d107"
- integrity sha512-Sw7Hm9VZ4ZE6knZNkd9L7vs1DGmeTFC1d0xzDytOKBw+1kK1+CpCLae2ehT+Kkkwho9GLwUFtHDdATEDLbFaBg==
+"@gitlab/ui@31.2.0":
+ version "31.2.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.2.0.tgz#7716500c9e811560d6e450d8553bf71bdcba79ec"
+ integrity sha512-hbW3Zd/gIN4C/AKx27ChZy4lf9yW8TBTJwG85dqQKSYvqWG3LuLx7o0kvc+UJqVFK3lk1iUC3pUSN2UrQ+isqg==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.18.1"