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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-24 00:11:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-24 00:11:46 +0300
commit66e3f84f5200d00e3ce3137dad80592096ef3401 (patch)
treed564786eec6b40a17c8450051887f949517d2454 /app
parent5421d61b1d5ffe11a9c7afbe2259b4e4d0e7c993 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue1
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_form.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue6
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue1
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue42
-rw-r--r--app/assets/javascripts/jobs/components/job/empty_state.vue24
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql16
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql17
-rw-r--r--app/assets/javascripts/jobs/components/job/job_app.vue22
-rw-r--r--app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue11
-rw-r--r--app/assets/javascripts/jobs/components/job/manual_variables_form.vue142
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue34
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue53
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue20
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue18
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue84
-rw-r--r--app/assets/javascripts/jobs/constants.js15
-rw-r--r--app/assets/javascripts/jobs/index.js9
-rw-r--r--app/assets/javascripts/lib/dompurify.js2
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/finders/git_refs_finder.rb53
-rw-r--r--app/graphql/types/commit_signatures/gpg_signature_type.rb1
-rw-r--r--app/graphql/types/commit_signatures/x509_signature_type.rb1
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/x509_helper.rb4
-rw-r--r--app/models/commit.rb10
-rw-r--r--app/models/commit_signatures/gpg_signature.rb9
-rw-r--r--app/models/commit_signatures/ssh_signature.rb9
-rw-r--r--app/models/commit_signatures/x509_commit_signature.rb9
-rw-r--r--app/models/concerns/commit_signature.rb4
-rw-r--r--app/models/concerns/signature_type.rb13
-rw-r--r--app/services/git/branch_hooks_service.rb21
-rw-r--r--app/views/projects/commit/_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml15
-rw-r--r--app/views/projects/commit/_signature_badge_user.html.haml22
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
36 files changed, 527 insertions, 174 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index c0cac958a42..fe18b9da560 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -312,6 +312,7 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
+ data-qa-selector="alert_table_container"
:items="
alerts
? alerts.list
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
index 388d925196b..a0d5cb7f4c3 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
@@ -83,7 +83,7 @@ export default {
</p>
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
- <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox">
+ <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox">
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 03bc4b825ae..65c3bc732ed 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -430,6 +430,7 @@ export default {
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
+ data-qa-selector="integration_type_dropdown"
:options="integrationTypesOptions"
/>
@@ -461,6 +462,7 @@ export default {
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
+ data-qa-selector="integration_name_field"
@input="validateName"
/>
</gl-form-group>
@@ -483,6 +485,7 @@ export default {
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
+ data-qa-selector="active_toggle_container"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
@@ -594,6 +597,7 @@ export default {
category="secondary"
class="gl-ml-3 js-no-auto-disable"
data-testid="integration-form-test-and-submit"
+ data-qa-selector="save_and_create_alert_button"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
@@ -695,6 +699,7 @@ export default {
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
+ data-qa-selector="test_payload_field"
@input="validateJson(false)"
/>
</gl-form-group>
@@ -706,6 +711,7 @@ export default {
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
+ data-qa-selector="send_test_alert_button"
@click="isFormDirty ? null : sendTestAlert()"
>
{{ $options.i18n.send }}
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index bf456b6adaa..010cb5721a1 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -375,6 +375,7 @@ export default {
category="secondary"
variant="confirm"
data-testid="add-integration-btn"
+ data-qa-selector="add_integration_button"
class="gl-mt-3"
@click="setFormVisibility(true)"
>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5f35dbdc5e7..3c9c0b1ade1 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
+ EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -26,6 +27,7 @@ import { performanceMarkAndMeasure } from '~/performance/utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
leftSidebarViews,
viewerTypes,
@@ -53,6 +55,7 @@ export default {
DiffViewer,
FileTemplatesBar,
},
+ mixins: [glFeatureFlagMixin()],
props: {
file: {
type: Object,
@@ -145,6 +148,12 @@ export default {
showTabs() {
return !this.shouldHideEditor && this.isEditModeActive && this.previewMode;
},
+ isCiConfigFile() {
+ return (
+ this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
+ this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ );
+ },
},
watch: {
'file.name': {
@@ -232,8 +241,6 @@ export default {
return;
}
- this.registerSchemaForFile();
-
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
@@ -357,6 +364,8 @@ export default {
this.model.updateOptions(this.rules);
+ this.registerSchemaForFile();
+
this.model.onChange((model) => {
const { file } = model;
if (!file.active) return;
@@ -446,8 +455,33 @@ export default {
return Promise.resolve();
},
registerSchemaForFile() {
- const schema = this.getJsonSchemaForPath(this.file.path);
- registerSchema(schema);
+ const registerExternalSchema = () => {
+ const schema = this.getJsonSchemaForPath(this.file.path);
+ return registerSchema(schema);
+ };
+ const registerLocalSchema = async () => {
+ if (!this.CiSchemaExtension) {
+ const { CiSchemaExtension } = await import(
+ '~/editor/extensions/source_editor_ci_schema_ext'
+ ).catch((e) =>
+ createAlert({
+ message: e,
+ }),
+ );
+ this.CiSchemaExtension = CiSchemaExtension;
+ }
+ this.editor.use({ definition: this.CiSchemaExtension });
+ this.editor.registerCiSchema();
+ };
+
+ if (this.isCiConfigFile && this.glFeatures.schemaLinting) {
+ registerLocalSchema();
+ } else {
+ if (this.CiSchemaExtension) {
+ this.editor.unuse(this.CiSchemaExtension);
+ }
+ registerExternalSchema();
+ }
},
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue
index 65b9600e664..053d5a4e740 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/job/empty_state.vue
@@ -20,6 +20,14 @@ export default {
type: String,
required: true,
},
+ isRetryable: {
+ type: Boolean,
+ required: false,
+ },
+ jobId: {
+ type: Number,
+ required: true,
+ },
title: {
type: String,
required: true,
@@ -54,8 +62,8 @@ export default {
},
},
computed: {
- isGraphQL() {
- return this.glFeatures?.graphqlJobApp;
+ showGraphQLManualVariablesForm() {
+ return this.glFeatures?.graphqlJobApp && this.isRetryable;
},
shouldRenderManualVariables() {
return this.playable && !this.scheduled;
@@ -77,14 +85,18 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <template v-if="isGraphQL">
- <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
+ <template v-if="showGraphQLManualVariablesForm">
+ <manual-variables-form
+ v-if="shouldRenderManualVariables"
+ :job-id="jobId"
+ @hideManualVariablesForm="$emit('hideManualVariablesForm')"
+ />
</template>
<template v-else>
<legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
</template>
- <div class="text-content">
- <div v-if="action && !shouldRenderManualVariables" class="text-center">
+ <div v-if="action && !shouldRenderManualVariables" class="text-content">
+ <div class="text-center">
<gl-link
:href="action.path"
:data-method="action.method"
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
new file mode 100644
index 00000000000..2b79892a072
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -0,0 +1,16 @@
+mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+ jobRetry(input: { id: $id, variables: $variables }) {
+ job {
+ id
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ webPath
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
new file mode 100644
index 00000000000..aaf1dec8e0f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
@@ -0,0 +1,17 @@
+query getJob($fullPath: ID!, $id: JobID!) {
+ project(fullPath: $fullPath) {
+ id
+ job(id: $id) {
+ id
+ manualJob
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue
index e5fbf77be1e..c6d900ef13e 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job/job_app.vue
@@ -72,6 +72,7 @@ export default {
data() {
return {
searchResults: [],
+ showUpdateVariablesState: false,
};
},
computed: {
@@ -122,6 +123,10 @@ export default {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
+ isJobRetryable() {
+ return Boolean(this.job.retry_path);
+ },
+
itemName() {
return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
},
@@ -169,10 +174,16 @@ export default {
'toggleScrollButtons',
'toggleScrollAnimation',
]),
+ onHideManualVariablesForm() {
+ this.showUpdateVariablesState = false;
+ },
onResize() {
this.updateSidebar();
this.updateScroll();
},
+ onUpdateVariables() {
+ this.showUpdateVariablesState = true;
+ },
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'xs' || breakpoint === 'sm') {
@@ -272,14 +283,12 @@ export default {
</div>
<!-- job log -->
<div
- v-if="hasJobLog"
+ v-if="hasJobLog && !showUpdateVariablesState"
class="build-log-container gl-relative"
:class="{ 'gl-mt-3': !job.archived }"
>
<log-top-bar
:class="{
- 'sidebar-expanded': isSidebarOpen,
- 'sidebar-collapsed': !isSidebarOpen,
'has-archived-block': job.archived,
}"
:size="jobLogSize"
@@ -300,14 +309,17 @@ export default {
<!-- empty state -->
<empty-state
- v-if="!hasJobLog"
+ v-if="!hasJobLog || showUpdateVariablesState"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
+ :is-retryable="isJobRetryable"
+ :job-id="job.id"
:title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
+ @hideManualVariablesForm="onHideManualVariablesForm()"
/>
<!-- EO empty state -->
@@ -321,9 +333,9 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
- :erase-path="job.erase_path"
:artifact-help-url="artifactHelpUrl"
data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
index 1898e02c94e..2b6b6f8e59e 100644
--- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
@@ -6,6 +6,7 @@ import {
GlButton,
GlLink,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
@@ -13,7 +14,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
- name: 'ManualVariablesForm',
+ name: 'LegacyManualVariablesForm',
components: {
GlFormInputGroup,
GlInputGroupText,
@@ -22,6 +23,9 @@ export default {
GlLink,
GlSprintf,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
action: {
type: Object,
@@ -42,6 +46,7 @@ export default {
value: 'value',
},
i18n: {
+ clearInputs: s__('CiVariables|Clear inputs'),
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
valueLabel: s__('CiVariables|Value'),
@@ -152,11 +157,13 @@ export default {
<gl-button
v-if="canRemove(index)"
+ v-gl-tooltip
+ :aria-label="$options.i18n.clearInputs"
+ :title="$options.i18n.clearInputs"
class="gl-flex-grow-0 gl-flex-basis-0"
category="tertiary"
variant="danger"
icon="clear"
- :aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
index 2f97301979c..e8edc7fc56f 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
@@ -5,15 +5,23 @@ import {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { mapActions } from 'vuex';
+import { cloneDeep, uniqueId } from 'lodash';
+import { fetchPolicies } from '~/lib/graphql';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import GetJob from './graphql/queries/get_job.query.graphql';
+import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql';
// This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue
-// It is meant to fetch the job information via GraphQL instead of REST API.
+// It is meant to fetch/update the job information via GraphQL instead of REST API.
export default {
name: 'ManualVariablesForm',
@@ -23,63 +31,73 @@ export default {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
},
- props: {
- action: {
- type: Object,
- required: false,
- default: null,
- validator(value) {
- return (
- value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'path') &&
- Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'button_title'))
- );
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ variables: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
+ return [...jobVariables.reverse(), ...this.variables];
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
},
},
},
+ props: {
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ },
inputTypes: {
key: 'key',
value: 'value',
},
i18n: {
+ clearInputs: s__('CiVariables|Clear inputs'),
+ 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',
+ ),
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
- valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
+ runAgainButtonText: s__('CiVariables|Run job again'),
+ valueLabel: s__('CiVariables|Value'),
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 {
+ job: {},
variables: [
{
- key: '',
- secretValue: '',
id: uniqueId(),
+ key: '',
+ value: '',
},
],
- triggerBtnDisabled: false,
+ runAgainBtnDisabled: false,
};
},
computed: {
variableSettings() {
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
- preparedVariables() {
- // we need to ensure no empty variables are passed to the API
- // and secretValue should be snake_case when passed to the API
- return this.variables
- .filter((variable) => variable.key !== '')
- .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
- },
},
methods: {
- ...mapActions(['triggerManualJob']),
addEmptyVariable() {
const lastVar = this.variables[this.variables.length - 1];
@@ -88,9 +106,9 @@ export default {
}
this.variables.push({
- key: '',
- secret_value: '',
id: uniqueId(),
+ key: '',
+ value: '',
});
},
canRemove(index) {
@@ -105,16 +123,45 @@ export default {
inputRef(type, id) {
return `${this.$options.inputTypes[type]}-${id}`;
},
- trigger() {
- this.triggerBtnDisabled = true;
+ navigateToRetriedJob(retryPath) {
+ redirectTo(retryPath);
+ },
+ async retryJob() {
+ try {
+ // filtering out 'id' along with empty variables to send only key, value in the mutation.
+ // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+ const preparedVariables = this.variables
+ .filter((variable) => variable.key !== '')
+ .map(({ key, value }) => ({ key, value }));
- this.triggerManualJob(this.preparedVariables);
+ const { data } = await this.$apollo.mutate({
+ mutation: retryJobWithVariablesMutation,
+ variables: {
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, this.jobId),
+ // we need to ensure no empty variables are passed to the API
+ variables: preparedVariables,
+ },
+ });
+ if (data.jobRetry?.errors?.length) {
+ createAlert({ message: data.jobRetry.errors[0] });
+ } else {
+ this.navigateToRetriedJob(data.jobRetry?.job?.webPath);
+ }
+ } catch (error) {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText });
+ }
+ },
+ runAgain() {
+ this.runAgainBtnDisabled = true;
+
+ this.retryJob();
},
},
};
</script>
<template>
- <div class="row gl-justify-content-center">
+ <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" />
+ <div v-else class="row gl-justify-content-center">
<div class="col-10" data-testid="manual-vars-form">
<label>{{ $options.i18n.header }}</label>
@@ -147,7 +194,7 @@ export default {
</template>
<gl-form-input
:ref="inputRef('value', variable.id)"
- v-model="variable.secretValue"
+ v-model="variable.value"
:placeholder="$options.i18n.valuePlaceholder"
data-testid="ci-variable-value"
/>
@@ -155,11 +202,13 @@ export default {
<gl-button
v-if="canRemove(index)"
+ v-gl-tooltip
+ :aria-label="$options.i18n.clearInputs"
+ :title="$options.i18n.clearInputs"
class="gl-flex-grow-0 gl-flex-basis-0"
category="tertiary"
variant="danger"
icon="clear"
- :aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
@@ -180,14 +229,21 @@ export default {
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-button
class="gl-mt-5"
+ :aria-label="__('Cancel')"
+ data-testid="cancel-btn"
+ @click="$emit('hideManualVariablesForm')"
+ >{{ __('Cancel') }}</gl-button
+ >
+ <gl-button
+ class="gl-mt-5"
variant="confirm"
category="primary"
- :aria-label="__('Trigger manual job')"
- :disabled="triggerBtnDisabled"
- data-testid="trigger-manual-job-btn"
- @click="trigger"
+ :aria-label="__('Run manual job again')"
+ :disabled="runAgainBtnDisabled"
+ data-testid="run-manual-job-btn"
+ @click="runAgain"
>
- {{ action.button_title }}
+ {{ $options.i18n.runAgainButtonText }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
index dd620977f0c..65175df555a 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
@@ -1,19 +1,23 @@
<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
export default {
name: 'JobSidebarRetryButton',
i18n: {
- retryLabel: JOB_SIDEBAR_COPY.retry,
+ ...JOB_SIDEBAR_COPY,
},
components: {
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
directives: {
GlModal: GlModalDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
modalId: {
type: String,
@@ -23,9 +27,16 @@ export default {
type: String,
required: true,
},
+ isManualJob: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
+ showRetryDropdown() {
+ return this.glFeatures?.graphqlJobApp && this.isManualJob;
+ },
},
};
</script>
@@ -33,17 +44,30 @@ export default {
<gl-button
v-if="hasForwardDeploymentFailure"
v-gl-modal="modalId"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
data-testid="retry-job-button"
/>
-
+ <gl-dropdown
+ v-else-if="showRetryDropdown"
+ icon="retry"
+ category="primary"
+ :right="true"
+ variant="confirm"
+ >
+ <gl-dropdown-item :href="href" data-method="post">
+ {{ $options.i18n.runAgainJobButtonLabel }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('updateVariablesClicked')">
+ {{ $options.i18n.updateVariables }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-button
v-else
:href="href"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue
new file mode 100644
index 00000000000..8a821b69f8c
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_job_sidebar_retry_button.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
+
+export default {
+ name: 'LegacyJobSidebarRetryButton',
+ i18n: {
+ retryLabel: JOB_SIDEBAR_COPY.retryJobLabel,
+ },
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ href: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['hasForwardDeploymentFailure']),
+ },
+};
+</script>
+<template>
+ <gl-button
+ v-if="hasForwardDeploymentFailure"
+ v-gl-modal="modalId"
+ :aria-label="$options.i18n.retryLabel"
+ category="primary"
+ variant="confirm"
+ icon="retry"
+ data-testid="retry-job-button"
+ />
+
+ <gl-button
+ v-else
+ :href="href"
+ :aria-label="$options.i18n.retryLabel"
+ category="primary"
+ variant="confirm"
+ icon="retry"
+ data-method="post"
+ data-testid="retry-job-link"
+ />
+</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
index 64b497c3550..5bbb831a293 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
@@ -2,7 +2,7 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/jobs/constants';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
export default {
@@ -25,20 +25,15 @@ export default {
required: true,
default: () => ({}),
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
retryButtonCategory() {
return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
},
buttonTitle() {
- return this.job.status && this.job.status.text === 'passed'
+ return this.job.status && this.job.status.text === PASSED_STATUS
? this.$options.i18n.runAgainJobButtonLabel
- : this.$options.i18n.retryJobButtonLabel;
+ : this.$options.i18n.retryJobLabel;
},
},
methods: {
@@ -50,17 +45,15 @@ export default {
<template>
<div class="gl-py-5 gl-display-flex gl-align-items-center">
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
+ ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">{{ job.name }}</h4>
</tooltip-on-truncate>
<div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
<gl-button
- v-if="erasePath"
+ v-if="job.erase_path"
v-gl-tooltip.left
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
+ :href="job.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
class="gl-mr-2"
data-testid="job-log-erase-link"
@@ -76,6 +69,7 @@ export default {
:category="retryButtonCategory"
:href="job.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
+ :is-manual-job="false"
variant="confirm"
data-qa-selector="retry_button"
data-testid="retry-button"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
index aac6a0ad6d3..02c3d60557b 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
@@ -2,13 +2,13 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import JobsContainer from './jobs_container.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
-import ArtifactsBlock from './artifacts_block.vue';
import LegacySidebarHeader from './legacy_sidebar_header.vue';
import SidebarHeader from './sidebar_header.vue';
import StagesDropdown from './stages_dropdown.vue';
@@ -41,11 +41,6 @@ export default {
required: false,
default: '',
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -89,8 +84,13 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" />
- <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" />
+ <sidebar-header
+ v-if="isGraphQL"
+ :rest-job="job"
+ :job-id="job.id"
+ @updateVariables="$emit('updateVariables')"
+ />
+ <legacy-sidebar-header v-else :job="job" />
<div
v-if="job.terminal_path || job.new_issue_path"
class="gl-py-5"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
index 523710598bf..c124f52ae79 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
@@ -1,8 +1,17 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import {
+ JOB_GRAPHQL_ERRORS,
+ GRAPHQL_ID_TYPES,
+ JOB_SIDEBAR_COPY,
+ forwardDeploymentFailureModalId,
+ PASSED_STATUS,
+} from '~/jobs/constants';
+import GetJob from '../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
// This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue
@@ -22,21 +31,58 @@ export default {
JobSidebarRetryButton,
TooltipOnTruncate,
},
- props: {
+ inject: ['projectPath'],
+ apollo: {
job: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ update(data) {
+ const { name, manualJob } = data?.project?.job || {};
+ return {
+ name,
+ manualJob,
+ };
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
+ },
+ },
+ },
+ props: {
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ restJob: {
type: Object,
required: true,
default: () => ({}),
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
+ },
+ data() {
+ return {
+ job: {},
+ };
},
computed: {
+ buttonTitle() {
+ return this.restJob.status?.text === PASSED_STATUS
+ ? this.$options.i18n.runAgainJobButtonLabel
+ : this.$options.i18n.retryJobLabel;
+ },
+ canShowJobRetryButton() {
+ return this.restJob.retry_path && !this.$apollo.queries.job.loading;
+ },
+ isManualJob() {
+ return this.job?.manualJob;
+ },
retryButtonCategory() {
- return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
+ return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
},
methods: {
@@ -48,17 +94,15 @@ export default {
<template>
<div class="gl-py-5 gl-display-flex gl-align-items-center">
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
+ ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
</tooltip-on-truncate>
<div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
<gl-button
- v-if="erasePath"
+ v-if="restJob.erase_path"
v-gl-tooltip.left
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
+ :href="restJob.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
class="gl-mr-2"
data-testid="job-log-erase-link"
@@ -67,23 +111,25 @@ export default {
icon="remove"
/>
<job-sidebar-retry-button
- v-if="job.retry_path"
+ v-if="canShowJobRetryButton"
v-gl-tooltip.left
- :title="$options.i18n.retryJobButtonLabel"
- :aria-label="$options.i18n.retryJobButtonLabel"
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
:category="retryButtonCategory"
- :href="job.retry_path"
+ :href="restJob.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
variant="confirm"
data-qa-selector="retry_button"
data-testid="retry-button"
+ @updateVariablesClicked="$emit('updateVariables')"
/>
<gl-button
- v-if="job.cancel_path"
+ v-if="restJob.cancel_path"
v-gl-tooltip.left
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
- :href="job.cancel_path"
+ :href="restJob.cancel_path"
variant="danger"
icon="cancel"
data-method="post"
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index e9475994e8b..405aea11181 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -5,6 +5,11 @@ const moreInfo = __('More information');
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
+export const GRAPHQL_ID_TYPES = {
+ commitStatus: 'CommitStatus',
+ ciBuild: 'Ci::Build',
+};
+
export const JOB_SIDEBAR_COPY = {
cancel,
cancelJobButtonLabel: s__('Job|Cancel'),
@@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = {
eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
newIssue: __('New issue'),
- retry: __('Retry'),
- retryJobButtonLabel: s__('Job|Retry'),
+ retryJobLabel: s__('Job|Retry'),
toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
+ updateVariables: s__('Job|Update CI/CD variables'),
+};
+
+export const JOB_GRAPHQL_ERRORS = {
+ retryMutationErrorText: __('There was an error running the job. Please try again.'),
+ jobQueryErrorText: __('There was an error fetching the job.'),
};
export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
@@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
};
export const SUCCESS_STATUS = 'SUCCESS';
+export const PASSED_STATUS = 'passed';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 9dd47f4046c..44bb1ffb1bc 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,17 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import JobApp from './components/job/job_app.vue';
import createStore from './store';
+Vue.use(VueApollo);
Vue.use(GlToast);
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
const initializeJobPage = (element) => {
const store = createStore();
@@ -26,11 +33,13 @@ const initializeJobPage = (element) => {
return new Vue({
el: element,
+ apolloProvider,
store,
components: {
JobApp,
},
provide: {
+ projectPath,
retryOutdatedJobDocsUrl,
},
render(createElement) {
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 27760e483aa..5372f6555d2 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -18,7 +18,7 @@ export const defaultConfig = {
'data-disable',
'data-turbo',
],
- FORBID_TAGS: ['style', 'mstyle'],
+ FORBID_TAGS: ['style', 'mstyle', 'form'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 19318d87731..dd24e3fcb5d 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -338,11 +338,6 @@
color: $gl-text-color;
}
-.commit .gpg-popover-help-link {
- display: block;
- color: $link-color;
-}
-
.add-review-item {
.gl-tab-nav-item {
height: 100%;
diff --git a/app/finders/git_refs_finder.rb b/app/finders/git_refs_finder.rb
index dbe0060d8ae..0492dd9934f 100644
--- a/app/finders/git_refs_finder.rb
+++ b/app/finders/git_refs_finder.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GitRefsFinder
+ include Gitlab::Utils::StrongMemoize
+
def initialize(repository, params = {})
@repository = repository
@params = params
@@ -10,44 +12,28 @@ class GitRefsFinder
attr_reader :repository, :params
- def search
- @params[:search].to_s.presence
- end
-
- def sort
- @params[:sort].to_s.presence || 'name'
- end
-
def by_search(refs)
return refs unless search
- case search
- when ->(v) { v.starts_with?('^') }
- filter_refs_with_prefix(refs, search.slice(1..-1))
- when ->(v) { v.ends_with?('$') }
- filter_refs_with_suffix(refs, search.chop)
- else
- matches = filter_refs_by_name(refs, search)
- set_exact_match_as_first_result(matches, search)
- end
- end
-
- def filter_refs_with_prefix(refs, prefix)
- prefix = prefix.downcase
+ matches = filter_refs(refs, search)
+ return matches if regex_search?
- refs.select { |ref| ref.name.downcase.starts_with?(prefix) }
+ set_exact_match_as_first_result(matches, search)
end
- def filter_refs_with_suffix(refs, suffix)
- suffix = suffix.downcase
-
- refs.select { |ref| ref.name.downcase.ends_with?(suffix) }
+ def search
+ @params[:search].to_s.presence
end
+ strong_memoize_attr :search
- def filter_refs_by_name(refs, term)
- term = term.downcase
+ def sort
+ @params[:sort].to_s.presence || 'name'
+ end
- refs.select { |ref| ref.name.downcase.include?(term) }
+ def filter_refs(refs, term)
+ regex_string = Regexp.quote(term.downcase)
+ regex_string = unescape_regex_operators(regex_string) if regex_search?
+ refs.select { |ref| /#{regex_string}/ === ref.name.downcase }
end
def set_exact_match_as_first_result(matches, term)
@@ -59,4 +45,13 @@ class GitRefsFinder
def find_exact_match_index(matches, term)
matches.index { |ref| ref.name.casecmp(term) == 0 }
end
+
+ def regex_search?
+ Regexp.union('^', '$', '*') === search
+ end
+ strong_memoize_attr :regex_search?, :regex_search
+
+ def unescape_regex_operators(regex_string)
+ regex_string.sub('\^', '^').gsub('\*', '.*?').sub('\$', '$')
+ end
end
diff --git a/app/graphql/types/commit_signatures/gpg_signature_type.rb b/app/graphql/types/commit_signatures/gpg_signature_type.rb
index 2a845fff3e2..3baf2d9d21d 100644
--- a/app/graphql/types/commit_signatures/gpg_signature_type.rb
+++ b/app/graphql/types/commit_signatures/gpg_signature_type.rb
@@ -11,6 +11,7 @@ module Types
authorize :download_code
field :user, Types::UserType, null: true,
+ method: :signed_by_user,
description: 'User associated with the key.'
field :gpg_key_user_name, GraphQL::Types::String,
diff --git a/app/graphql/types/commit_signatures/x509_signature_type.rb b/app/graphql/types/commit_signatures/x509_signature_type.rb
index 9ac96dbc015..2d58c3d5b5d 100644
--- a/app/graphql/types/commit_signatures/x509_signature_type.rb
+++ b/app/graphql/types/commit_signatures/x509_signature_type.rb
@@ -11,6 +11,7 @@ module Types
authorize :download_code
field :user, Types::UserType, null: true,
+ method: :signed_by_user,
calls_gitaly: true,
description: 'User associated with the key.'
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index e05adc5cd0e..6868fed711e 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -227,7 +227,7 @@ module DiffHelper
end
def conflicts(allow_tree_conflicts: false)
- return unless merge_request.cannot_be_merged?
+ return unless merge_request.cannot_be_merged? && merge_request.source_branch_exists? && merge_request.target_branch_exists?
conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
diff --git a/app/helpers/x509_helper.rb b/app/helpers/x509_helper.rb
index 1a9dbefceef..599be0b91f7 100644
--- a/app/helpers/x509_helper.rb
+++ b/app/helpers/x509_helper.rb
@@ -16,8 +16,4 @@ module X509Helper
rescue StandardError
{}
end
-
- def x509_signature?(sig)
- sig.is_a?(CommitSignatures::X509CommitSignature) || sig.is_a?(Gitlab::X509::Signature)
- end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 54de45ebba7..5175842e5de 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -359,6 +359,10 @@ class Commit
end
def has_signature?
+ if signature_type == :SSH && !ssh_signatures_enabled?
+ return false
+ end
+
signature_type && signature_type != :NONE
end
@@ -378,6 +382,10 @@ class Commit
@signature_type ||= raw_signature_type || :NONE
end
+ def ssh_signatures_enabled?
+ Feature.enabled?(:ssh_commit_signatures, project)
+ end
+
def signature
strong_memoize(:signature) do
case signature_type
@@ -385,6 +393,8 @@ class Commit
gpg_commit.signature
when :X509
Gitlab::X509::Commit.new(self).signature
+ when :SSH
+ Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled?
else
nil
end
diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb
index 2ae59853520..a9e8ca2dd33 100644
--- a/app/models/commit_signatures/gpg_signature.rb
+++ b/app/models/commit_signatures/gpg_signature.rb
@@ -2,6 +2,7 @@
module CommitSignatures
class GpgSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
sha_attribute :gpg_key_primary_keyid
@@ -10,6 +11,14 @@ module CommitSignatures
validates :gpg_key_primary_keyid, presence: true
+ def signed_by_user
+ gpg_key&.user
+ end
+
+ def type
+ :gpg
+ end
+
def self.with_key_and_subkeys(gpg_key)
subkey_ids = gpg_key.subkeys.pluck(:id)
diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb
index 7a8d0653fcd..1e64e2b2978 100644
--- a/app/models/commit_signatures/ssh_signature.rb
+++ b/app/models/commit_signatures/ssh_signature.rb
@@ -3,7 +3,16 @@
module CommitSignatures
class SshSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :key, optional: true
+
+ def type
+ :ssh
+ end
+
+ def signed_by_user
+ key&.user
+ end
end
end
diff --git a/app/models/commit_signatures/x509_commit_signature.rb b/app/models/commit_signatures/x509_commit_signature.rb
index 2cbb331dd7e..4edbc147502 100644
--- a/app/models/commit_signatures/x509_commit_signature.rb
+++ b/app/models/commit_signatures/x509_commit_signature.rb
@@ -2,15 +2,24 @@
module CommitSignatures
class X509CommitSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false
validates :x509_certificate_id, presence: true
+ def type
+ :x509
+ end
+
def x509_commit
return unless commit
Gitlab::X509::Commit.new(commit)
end
+
+ def signed_by_user
+ commit&.committer
+ end
end
end
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdfa9a2966..7f1fbbefd94 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -44,7 +44,7 @@ module CommitSignature
project.commit(commit_sha)
end
- def user
- commit.committer
+ def signed_by_user
+ raise NoMethodError, 'must implement `signed_by_user` method'
end
end
diff --git a/app/models/concerns/signature_type.rb b/app/models/concerns/signature_type.rb
new file mode 100644
index 00000000000..804f42b6f72
--- /dev/null
+++ b/app/models/concerns/signature_type.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SignatureType
+ TYPES = %i[gpg ssh x509].freeze
+
+ def type
+ raise NoMethodError, 'must implement `type` method'
+ end
+
+ TYPES.each do |type|
+ define_method("#{type}?") { self.type == type }
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 7de56c037ed..71dd9501648 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -164,16 +164,27 @@ module Git
end
end
- def unsigned_x509_shas(commits)
- CommitSignatures::X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
+ def signature_types
+ types = [
+ ::CommitSignatures::GpgSignature,
+ ::CommitSignatures::X509CommitSignature
+ ]
+
+ types.push(::CommitSignatures::SshSignature) if Feature.enabled?(:ssh_commit_signatures, project)
+
+ types
end
- def unsigned_gpg_shas(commits)
- CommitSignatures::GpgSignature.unsigned_commit_shas(commits.map(&:sha))
+ def unsigned_commit_shas(commits)
+ commit_shas = commits.map(&:sha)
+
+ signature_types
+ .map { |signature| signature.unsigned_commit_shas(commit_shas) }
+ .reduce(&:&)
end
def enqueue_update_signatures
- unsigned = unsigned_x509_shas(limited_commits) & unsigned_gpg_shas(limited_commits)
+ unsigned = unsigned_commit_shas(limited_commits)
return if unsigned.empty?
signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
index 978d83bf2b4..c6f1e51049e 100644
--- a/app/views/projects/commit/_signature.html.haml
+++ b/app/views/projects/commit/_signature.html.haml
@@ -1,3 +1,3 @@
- if signature
- - uri = "projects/commit/#{'x509/' if x509_signature?(signature)}"
+ - uri = "projects/commit/#{'x509/' if signature.x509?}"
= render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index fb30bfc2953..ad6b524c01b 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,18 +17,23 @@
- content = capture do
- if show_user
.clearfix
- - uri_signature_badge_user = "projects/commit/#{'x509/' if x509_signature?(signature)}signature_badge_user"
+ - uri_signature_badge_user = "projects/commit/#{'x509/' if signature.x509?}signature_badge_user"
= render partial: "#{uri_signature_badge_user}", locals: { signature: signature }
- - if x509_signature?(signature)
+ - if signature.x509?
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
- = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gl-link gl-display-block')
+ - elsif ::Feature.enabled?(:ssh_commit_signatures, signature.project) && signature.ssh?
+ = _('SSH key fingerprint:')
+ %span.gl-font-monospace= signature.key&.fingerprint_sha256 || _('Unknown')
+
+ = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/ssh_signed_commits/index.md'), class: 'gl-link gl-display-block')
- else
= _('GPG Key ID:')
- %span.monospace= signature.gpg_key_primary_keyid
+ %span.gl-font-monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link gl-display-block')
+ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gl-link gl-display-block')
%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml
index b20198e76db..656adef6a72 100644
--- a/app/views/projects/commit/_signature_badge_user.html.haml
+++ b/app/views/projects/commit/_signature_badge_user.html.haml
@@ -1,7 +1,4 @@
-- gpg_key = signature.gpg_key
-- user = gpg_key&.user
-- user_name = signature.gpg_key_user_name
-- user_email = signature.gpg_key_user_email
+- user = signature.signed_by_user
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
@@ -11,11 +8,14 @@
%div
%strong= user.name
%div= user.to_reference
-- else
- = mail_to user_email do
- %div
- = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
+- elsif signature.gpg? # SSH signatures do not have an email embedded in them
+ - user_name = signature.gpg_key_user_name
+ - user_email = signature.gpg_key_user_email
+ - if user_name && user_email
+ = mail_to user_email do
+ %div
+ = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
- %div
- %strong= user_name
- %div= user_email
+ %div
+ %strong= user_name
+ %div= user_email
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index d80f1e4597c..7433e81c11c 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -3,7 +3,7 @@
- add_page_specific_style 'page_bundles/alert_management_settings'
- add_page_specific_style 'page_bundles/incident_management_list'
-%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded), data: { qa_selector: 'alerts_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Alerts')