diff options
52 files changed, 1187 insertions, 924 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 3a210c358e5..5c1b669a88f 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -15,6 +15,7 @@ MUST be linked for the release bot to know that the associated merge requests sh - Fill out the [Links section](#links): - [ ] Next to **Issue on GitLab**, add a link to the `gitlab-org/gitlab` issue that describes the security vulnerability. - [ ] Add one of the `~severity::x` labels to the issue and all associated merge requests. +- [ ] If this change affects the public interface (public API or UI) of the product, post in the `#support_gitlab-com` Slack channel to explain the impact and discuss a mitigation plan for users that might be affected. If you need Support feedback or approval, reach out in `#spt_managers` Slack channel or mention `@gitlab-com/support/managers`. ## Development diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 37fe4887872..cadc3650a89 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -e023bdab6cea4ad7124d64b12e77f2cdb0681e21 +cd3f92195e1c23abd22d4dfcb43c6f9ad3d817d4 diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index 977da12e8a9..36b9b647af7 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -12,10 +12,10 @@ export default { <template> <div class="empty-state"> <div class="text-content"> - <h4 class="blank-state-title js-blank-state-title"> + <h4 class="js-blank-state-title"> {{ s__("Environments|You don't have any environments right now") }} </h4> - <p class="blank-state-text"> + <p> {{ s__(`Environments|Environments are places where code gets deployed, such as staging or production.`) diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 184bda4410f..e7bac11a27e 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -199,6 +199,7 @@ export default { requestAccessEnabled: true, highlightChangesClass: false, emailsDisabled: false, + showDiffPreviewInEmail: true, cveIdRequestEnabled: true, featureAccessLevelEveryone, featureAccessLevelMembers, @@ -762,6 +763,24 @@ export default { </project-setting-row> <project-setting-row class="mb-3"> <input + :value="showDiffPreviewInEmail" + type="hidden" + name="project[project_setting_attributes][show_diff_preview_in_email]" + /> + <gl-form-checkbox + v-model="showDiffPreviewInEmail" + name="project[project_setting_attributes][show_diff_preview_in_email]" + > + {{ s__('ProjectSettings|Include diff preview in merge request notification emails') }} + <template #help>{{ + s__( + 'ProjectSettings|Include the code diff preview on comment threads in merge request notification emails.', + ) + }}</template> + </gl-form-checkbox> + </project-setting-row> + <project-setting-row class="mb-3"> + <input :value="showDefaultAwardEmojis" type="hidden" name="project[project_setting_attributes][show_default_award_emojis]" diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index bc83844b8b9..b003302ec8e 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -7,10 +7,11 @@ const DEFERRED_LINK_CLASS = 'deferred-link'; export default class PersistentUserCallout { constructor(container, options = container.dataset) { - const { dismissEndpoint, featureId, deferLinks } = options; + const { dismissEndpoint, featureId, groupId, deferLinks } = options; this.container = container; this.dismissEndpoint = dismissEndpoint; this.featureId = featureId; + this.groupId = groupId; this.deferLinks = parseBoolean(deferLinks); this.init(); @@ -52,6 +53,7 @@ export default class PersistentUserCallout { axios .post(this.dismissEndpoint, { feature_name: this.featureId, + group_id: this.groupId, }) .then(() => { this.container.remove(); diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index a7f8704b559..337c204c36a 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -10,6 +10,7 @@ const PERSISTENT_USER_CALLOUTS = [ '.js-new-user-signups-cap-reached', '.js-eoa-bronze-plan-banner', '.js-security-newsletter-callout', + '.js-approaching-seats-count-threshold', ]; const initCallouts = () => { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c1c8bfffff7..8e43a9b1b0d 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -46,7 +46,6 @@ @import 'framework/toggle'; @import 'framework/typography'; @import 'framework/zen'; -@import 'framework/blank'; @import 'framework/wells'; @import 'framework/page_header'; @import 'framework/page_title'; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss deleted file mode 100644 index 7dd7ab339dd..00000000000 --- a/app/assets/stylesheets/framework/blank.scss +++ /dev/null @@ -1,118 +0,0 @@ -.blank-state-parent-container { - .section-container { - padding: 10px; - } - - .section-body { - width: 100%; - height: 100%; - padding-bottom: 25px; - border-radius: $border-radius-default; - } -} - -.blank-state-row { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - -.blank-state-welcome { - text-align: center; - padding: $gl-padding 0 ($gl-padding * 2); - - .blank-state-welcome-title { - font-size: 24px; - } - - .blank-state-text { - margin-bottom: 0; - } -} - -.blank-state-link { - color: $gl-text-color; - margin-bottom: 15px; - - &:hover { - background-color: $gray-light; - text-decoration: none; - color: $gl-text-color; - } -} - -.blank-state-center { - padding-top: 20px; - padding-bottom: 20px; - text-align: center; -} - -.blank-state { - display: flex; - align-items: center; - padding: 20px 50px; - border: 1px solid $border-color; - border-radius: $border-radius-default; - min-height: 240px; - margin-bottom: $gl-padding; - width: calc(50% - #{$gl-padding-8}); - - @include media-breakpoint-down(sm) { - width: 100%; - flex-direction: column; - justify-content: center; - padding: 50px 20px; - - .column-small & { - width: 100%; - } - - } -} - -.blank-state, -.blank-state-center { - .blank-state-icon { - svg { - display: block; - margin: auto; - } - } - - .blank-state-title { - margin-top: 0; - font-size: 18px; - } - - .blank-state-body { - @include media-breakpoint-down(sm) { - text-align: center; - margin-top: 20px; - } - - @include media-breakpoint-up(sm) { - padding-left: 20px; - } - } -} - -@include media-breakpoint-up(lg) { - .column-large { - flex: 2; - } - - .column-small { - flex: 1; - margin-bottom: 15px; - - .blank-state { - max-width: 400px; - flex-wrap: wrap; - margin-left: 15px; - } - - .blank-state-icon { - margin-bottom: 30px; - } - } -} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f9b0f4f3118..9cf435af726 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -929,8 +929,6 @@ Merge requests */ $mr-tabs-height: 48px; $mr-version-controls-height: 56px; -$mr-widget-margin-left: 40px; -$mr-review-bar-height: calc(2rem + 13px); /* Compare Branches diff --git a/app/assets/stylesheets/page_bundles/dashboard_projects.scss b/app/assets/stylesheets/page_bundles/dashboard_projects.scss new file mode 100644 index 00000000000..eb0e1701b7f --- /dev/null +++ b/app/assets/stylesheets/page_bundles/dashboard_projects.scss @@ -0,0 +1,35 @@ +@import 'mixins_and_variables_and_functions'; + +.blank-state { + padding: 20px 50px; + min-height: 240px; + width: calc(50% - #{$gl-padding-8}); + + @include media-breakpoint-down(sm) { + width: 100%; + flex-direction: column; + justify-content: center; + padding: 50px 20px; + } +} + +.blank-state-link { + &:hover { + background-color: $gray-light; + text-decoration: none; + color: $gl-text-color; + } +} + +.blank-state-icon { + svg { + display: block; + } +} + +.blank-state-body { + @include media-breakpoint-down(sm) { + text-align: center; + margin-top: 20px; + } +} diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index 75b953df791..1ee154f9aeb 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -1,5 +1,10 @@ @import 'mixins_and_variables_and_functions'; +$mr-review-bar-height: calc(2rem + 13px); +$mr-widget-margin-left: 40px; +$mr-widget-min-height: 69px; +$tabs-holder-z-index: 250; + .compare-versions-container { min-width: 0; } @@ -108,6 +113,638 @@ } } +.ci-widget-container { + justify-content: space-between; + flex: 1; + flex-direction: row; + + @include media-breakpoint-down(sm) { + flex-direction: column; + + .stage-cell .stage-container { + margin-top: 16px; + } + + .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu { + transform: initial; + } + } + + .coverage { + font-size: 12px; + color: $gray-500; + line-height: initial; + } +} + +.deploy-body { + display: flex; + align-items: center; + flex-wrap: wrap; + + @include media-breakpoint-up(xs) { + flex-wrap: nowrap; + white-space: nowrap; + } + + @include media-breakpoint-down(md) { + flex-direction: column; + align-items: flex-start; + + .deployment-info { + margin-bottom: $gl-padding; + } + } + + > *:not(:last-child) { + margin-right: 0.3em; + } + + svg { + vertical-align: text-top; + } + + .deployment-info { + flex: 1; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 100px; + + @include media-breakpoint-up(xs) { + min-width: 0; + max-width: 100%; + } + } + + .dropdown-menu { + width: 400px; + } +} + +.deploy-heading, +.merge-train-position-indicator { + @include media-breakpoint-up(md) { + padding: $gl-padding-8 $gl-padding; + } + + .media-body { + min-width: 0; + font-size: 12px; + margin-left: 32px; + } + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } +} + +.diff-file-row.is-active { + background-color: $gray-50; +} + +.mr-conflict-loader { + max-width: 334px; + + > svg { + vertical-align: middle; + } +} + +.mr-info-list { + clear: left; + position: relative; + padding-top: 4px; + + p { + margin: 0; + position: relative; + padding: 4px 0; + + &:last-child { + padding-bottom: 0; + } + } + + &.mr-memory-usage { + p { + float: left; + } + + .memory-graph-container { + float: left; + margin-left: 5px; + } + } +} + +.mr-memory-usage { + width: 100%; + + p.usage-info-loading .usage-info-load-spinner { + margin-right: 10px; + font-size: 16px; + } +} + +.mr-ready-to-merge-loader { + max-width: 418px; + + > svg { + vertical-align: middle; + } +} + +.mr-section-container { + border: 1px solid $border-color; + border-radius: $border-radius-default; + background: var(--white, $white); + + > .mr-widget-border-top:first-of-type { + border-top: 0; + } +} + +.mr-source-target { + flex-wrap: wrap; + padding: $gl-padding; + background: var(--white, $white); + min-height: $mr-widget-min-height; + + @include media-breakpoint-up(md) { + align-items: center; + } + + .git-merge-container { + justify-content: space-between; + flex: 1; + flex-direction: row; + align-items: center; + + @include media-breakpoint-down(md) { + flex-direction: column; + align-items: stretch; + + .branch-actions { + margin-top: 16px; + } + } + + @include media-breakpoint-up(lg) { + .branch-actions { + align-self: center; + margin-left: $gl-padding; + white-space: nowrap; + } + } + } + + .diverged-commits-count { + color: $gl-text-color-secondary; + } +} + +.mr-state-widget { + color: $gl-text-color; + + .commit-message-edit { + border-radius: $border-radius-default; + } + + .mr-widget-section:not(:first-child) { + border-top: solid 1px $border-color; + } + + .mr-widget-alert-container + .mr-widget-section { + border-top: 0; + } + + .mr-fast-forward-message { + padding-left: $gl-padding-50; + padding-bottom: $gl-padding; + } + + .commits-list { + > li { + padding: $gl-padding; + + @include media-breakpoint-up(md) { + margin-left: $gl-spacing-scale-7; + } + } + } + + .mr-commit-dropdown { + .dropdown-menu { + @include media-breakpoint-up(md) { + width: 150%; + } + } + } + + .mr-report { + padding: 0; + + > .media { + padding: $gl-padding; + } + } + + form { + margin-bottom: 0; + + .clearfix { + margin-bottom: 0; + } + } + + label { + margin-bottom: 0; + } + + .btn { + font-size: $gl-font-size; + } + + .accept-merge-holder { + .accept-action { + display: inline-block; + float: left; + } + } + + .ci-widget { + color: $gl-text-color; + display: flex; + align-items: center; + justify-content: space-between; + + @include media-breakpoint-down(xs) { + flex-wrap: wrap; + } + + .ci-widget-content { + display: flex; + align-items: center; + flex: 1; + } + } + + .mr-widget-icon { + font-size: 22px; + } + + .mr-loading-icon { + margin: 3px 0; + } + + .ci-status-icon svg { + margin: 3px 0; + position: relative; + overflow: visible; + display: block; + } + + .mr-widget-pipeline-graph { + .dropdown-menu { + z-index: $zindex-dropdown-menu; + } + } + + .normal { + flex: 1; + flex-basis: auto; + } + + .capitalize { + text-transform: capitalize; + } + + .label-branch { + @include gl-font-monospace; + font-size: 95%; + color: $gl-text-color; + font-weight: normal; + overflow: hidden; + word-break: break-all; + } + + .deploy-link, + .label-branch { + &.label-truncate { + // NOTE: This selector targets its children because some of the HTML comes from + // 'source_branch_link'. Once this external HTML is no longer used, we could + // simplify this. + > a, + > span { + display: inline-block; + max-width: 12.5em; + margin-bottom: -6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + .mr-widget-body { + &:not(.mr-widget-body-line-height-1) { + line-height: 28px; + } + + @include clearfix; + + .approve-btn { + margin-right: 5px; + } + + h4 { + float: left; + font-weight: $gl-font-weight-bold; + font-size: 14px; + line-height: inherit; + margin-top: 0; + margin-bottom: 0; + + time { + font-weight: $gl-font-weight-normal; + } + } + + .btn-grouped { + margin-left: 0; + margin-right: 7px; + } + + label { + font-weight: $gl-font-weight-normal; + } + + .spacing { + margin: 0 0 0 10px; + } + + .bold, + .gl-font-weight-bold { + font-weight: $gl-font-weight-bold; + color: $gray-600; + margin-left: 10px; + } + + .state-label { + font-weight: $gl-font-weight-bold; + padding-right: 10px; + } + + .danger { + color: $red-500; + } + + .spacing, + .bold, + .gl-font-weight-bold { + vertical-align: middle; + } + + .dropdown-menu { + li a { + padding: 5px; + } + + .merge-opt-icon { + line-height: 1.5; + } + + .merge-opt-title { + margin-left: 8px; + } + } + + .has-custom-error { + display: inline-block; + } + + @include media-breakpoint-down(xs) { + p { + font-size: 13px; + } + + .btn-grouped { + float: none; + margin-right: 0; + } + + .accept-action { + width: 100%; + text-align: center; + } + } + + .commit-message-editor { + label { + padding: 0; + } + } + + &.mr-widget-empty-state { + line-height: 20px; + padding: $gl-padding; + + .artwork { + + @include media-breakpoint-down(md) { + margin-bottom: $gl-padding; + } + } + + .text { + p { + margin-top: $gl-padding; + } + + .highlight { + margin: 0 0 $gl-padding; + font-weight: $gl-font-weight-bold; + } + } + } + + &.mr-pipeline-suggest { + border-radius: $border-radius-default; + line-height: 20px; + border: 1px solid $border-color; + + .circle-icon-container { + color: $gl-text-color-quaternary; + } + } + } + + .ci-coverage { + float: right; + } + + .stop-env-container { + color: $gl-text-color; + float: right; + + a { + color: $gl-text-color; + } + } +} + +.mr-widget-alert-container { + $radius: $border-radius-default - 1px; + + border-radius: $radius $radius 0 0; + + .gl-alert:not(:last-child) { + margin-bottom: 1px; + } +} + +.mr-widget-body, +.mr-widget-content { + padding: $gl-padding; +} + +.mr-widget-border-top { + border-top: 1px solid $border-color; +} + +.mr-widget-extension { + border-top: 1px solid $border-color; + background-color: $gray-50; + + &.clickable:hover { + background-color: $gray-100; + cursor: pointer; + } +} + +.mr-widget-extension-icon::before { + @include gl-content-empty; + @include gl-absolute; + @include gl-left-0; + @include gl-top-0; + @include gl-opacity-3; + @include gl-border-solid; + @include gl-border-4; + @include gl-rounded-full; + + width: 24px; + height: 24px; +} + +.mr-widget-heading { + position: relative; + border: 1px solid $border-color; + border-radius: $border-radius-default; + background: var(--white, $white); + + .gl-skeleton-loader { + display: block; + } +} + +.mr-widget-info { + padding-left: $gl-padding; + padding-right: $gl-padding; +} + +.mr-widget-margin-left { + margin-left: $mr-widget-margin-left; +} + +.mr-widget-section { + .code-text { + flex: 1; + } +} + +.mr-widget-workflow { + margin-top: $gl-padding; + position: relative; + + &::before { + content: ''; + border-left: 1px solid $gray-100; + position: absolute; + left: 28px; + top: -17px; + height: 16px; + } +} + +.mr-version-controls { + position: relative; + z-index: $tabs-holder-z-index + 10; + background: $white; + color: $gl-text-color; + margin-top: -1px; + + .mr-version-menus-container { + display: flex; + align-items: center; + flex-wrap: wrap; + padding: 16px; + z-index: 199; + white-space: nowrap; + + .gl-dropdown-toggle { + width: auto; + max-width: 170px; + + svg { + top: 10px; + right: 8px; + } + } + } + + .content-block { + padding: $gl-padding; + border-bottom: 0; + } + + .mr-version-dropdown, + .mr-version-compare-dropdown { + margin: 0 0.5rem; + } + + .dropdown-title { + color: $gl-text-color; + } + + // Shortening button height by 1px to make compare-versions + // header 56px and fit into our 8px design grid + .btn { + height: 34px; + } + + @include media-breakpoint-up(md) { + position: -webkit-sticky; + position: sticky; + top: calc(#{$header-height} + #{$mr-tabs-height}); + + .with-system-header & { + top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height}); + } + + .with-system-header.with-performance-bar & { + top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height} + #{$performance-bar-height}); + } + + .mr-version-menus-container { + flex-wrap: nowrap; + } + + .with-performance-bar & { + top: calc(#{$header-height} + #{$performance-bar-height} + #{$mr-tabs-height}); + } + } +} + // TODO: Move to GitLab UI .mr-extenson-scrim { background: linear-gradient(to bottom, rgba($gray-light, 0), rgba($gray-light, 1)); diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b858d457969..dc01aea7681 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -2,8 +2,6 @@ * MR -> show: Automerge widget * */ - -$mr-widget-min-height: 69px; $tabs-holder-z-index: 250; .space-children { @@ -18,12 +16,6 @@ $tabs-holder-z-index: 250; } } -.mr-widget-border-top { - border-top: 1px solid $border-color; -} - -.mr-widget-margin-left { margin-left: $mr-widget-margin-left; } - .media-section { @include media-breakpoint-down(md) { align-items: flex-start; @@ -42,140 +34,9 @@ $tabs-holder-z-index: 250; } } -.mr-widget-section { - .code-text { - flex: 1; - } -} - -.mr-widget-heading { - position: relative; - border: 1px solid $border-color; - border-radius: $border-radius-default; - background: var(--white, $white); - - .gl-skeleton-loader { - display: block; - } -} - -.mr-widget-extension { - border-top: 1px solid $border-color; - background-color: $gray-50; - - &.clickable:hover { - background-color: $gray-100; - cursor: pointer; - } -} - -.mr-widget-workflow { - margin-top: $gl-padding; - position: relative; - - &::before { - content: ''; - border-left: 1px solid $gray-100; - position: absolute; - left: 28px; - top: -17px; - height: 16px; - } -} - -.mr-section-container { - border: 1px solid $border-color; - border-radius: $border-radius-default; - background: var(--white, $white); - - > .mr-widget-border-top:first-of-type { - border-top: 0; - } -} - -.mr-widget-body, -.mr-widget-content, -.mr-widget-footer { - padding: $gl-padding; -} - -.mr-widget-info { - padding-left: $gl-padding; - padding-right: $gl-padding; -} - .mr-state-widget { - color: $gl-text-color; - - .commit-message-edit { - border-radius: $border-radius-default; - } - - .mr-widget-section:not(:first-child), - .mr-widget-footer { - border-top: solid 1px $border-color; - } - - .mr-widget-alert-container + .mr-widget-section { - border-top: 0; - } - - .mr-fast-forward-message { - padding-left: $gl-padding-50; - padding-bottom: $gl-padding; - } - - .commits-list { - > li { - padding: $gl-padding; - - @include media-breakpoint-up(md) { - margin-left: $gl-spacing-scale-7; - } - } - } - - .mr-commit-dropdown { - .dropdown-menu { - @include media-breakpoint-up(md) { - width: 150%; - } - } - } - - .mr-widget-footer { - padding: 0; - } - - .mr-report { - padding: 0; - - > .media { - padding: $gl-padding; - } - } - - form { - margin-bottom: 0; - - .clearfix { - margin-bottom: 0; - } - } - - label { - margin-bottom: 0; - } - - .btn { - font-size: $gl-font-size; - } - .accept-merge-holder { .accept-action { - display: inline-block; - float: left; - .accept-merge-request { &.ci-preparing, &.ci-pending, @@ -192,226 +53,6 @@ $tabs-holder-z-index: 250; } } } - - .ci-widget { - color: $gl-text-color; - display: flex; - align-items: center; - justify-content: space-between; - - @include media-breakpoint-down(xs) { - flex-wrap: wrap; - } - - .ci-widget-content { - display: flex; - align-items: center; - flex: 1; - } - } - - .mr-widget-icon { - font-size: 22px; - } - - .mr-loading-icon { - margin: 3px 0; - } - - .ci-status-icon svg { - margin: 3px 0; - position: relative; - overflow: visible; - display: block; - } - - .mr-widget-pipeline-graph { - .dropdown-menu { - z-index: $zindex-dropdown-menu; - } - } - - .normal { - flex: 1; - flex-basis: auto; - } - - .capitalize { - text-transform: capitalize; - } - - .label-branch { - @include gl-font-monospace; - font-size: 95%; - color: $gl-text-color; - font-weight: normal; - overflow: hidden; - word-break: break-all; - } - - .deploy-link, - .label-branch { - &.label-truncate { - // NOTE: This selector targets its children because some of the HTML comes from - // 'source_branch_link'. Once this external HTML is no longer used, we could - // simplify this. - > a, - > span { - display: inline-block; - max-width: 12.5em; - margin-bottom: -6px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - - .mr-widget-body { - &:not(.mr-widget-body-line-height-1) { - line-height: 28px; - } - - @include clearfix; - - .approve-btn { - margin-right: 5px; - } - - h4 { - float: left; - font-weight: $gl-font-weight-bold; - font-size: 14px; - line-height: inherit; - margin-top: 0; - margin-bottom: 0; - - time { - font-weight: $gl-font-weight-normal; - } - } - - .btn-grouped { - margin-left: 0; - margin-right: 7px; - } - - label { - font-weight: $gl-font-weight-normal; - } - - .spacing { - margin: 0 0 0 10px; - } - - .bold, - .gl-font-weight-bold { - font-weight: $gl-font-weight-bold; - color: $gray-600; - margin-left: 10px; - } - - .state-label { - font-weight: $gl-font-weight-bold; - padding-right: 10px; - } - - .danger { - color: $red-500; - } - - .spacing, - .bold, - .gl-font-weight-bold { - vertical-align: middle; - } - - .dropdown-menu { - li a { - padding: 5px; - } - - .merge-opt-icon { - line-height: 1.5; - } - - .merge-opt-title { - margin-left: 8px; - } - } - - .has-custom-error { - display: inline-block; - } - - @include media-breakpoint-down(xs) { - p { - font-size: 13px; - } - - .btn-grouped { - float: none; - margin-right: 0; - } - - .accept-action { - width: 100%; - text-align: center; - } - } - - .commit-message-editor { - label { - padding: 0; - } - } - - &.mr-widget-empty-state { - line-height: 20px; - padding: $gl-padding; - - .artwork { - - @include media-breakpoint-down(md) { - margin-bottom: $gl-padding; - } - } - - .text { - p { - margin-top: $gl-padding; - } - - .highlight { - margin: 0 0 $gl-padding; - font-weight: $gl-font-weight-bold; - } - } - } - - &.mr-pipeline-suggest { - border-radius: $border-radius-default; - line-height: 20px; - border: 1px solid $border-color; - - .circle-icon-container { - color: $gl-text-color-quaternary; - } - } - } - - .ci-coverage { - float: right; - } - - .stop-env-container { - color: $gl-text-color; - float: right; - - a { - color: $gl-text-color; - } - } } .mr_source_commit, @@ -477,72 +118,6 @@ $tabs-holder-z-index: 250; } } -.mr-info-list { - clear: left; - position: relative; - padding-top: 4px; - - p { - margin: 0; - position: relative; - padding: 4px 0; - - &:last-child { - padding-bottom: 0; - } - } - - &.mr-memory-usage { - p { - float: left; - } - - .memory-graph-container { - float: left; - margin-left: 5px; - } - } -} - -.mr-source-target { - flex-wrap: wrap; - padding: $gl-padding; - background: var(--white, $white); - min-height: $mr-widget-min-height; - - @include media-breakpoint-up(md) { - align-items: center; - } - - .git-merge-container { - justify-content: space-between; - flex: 1; - flex-direction: row; - align-items: center; - - @include media-breakpoint-down(md) { - flex-direction: column; - align-items: stretch; - - .branch-actions { - margin-top: 16px; - } - } - - @include media-breakpoint-up(lg) { - .branch-actions { - align-self: center; - margin-left: $gl-padding; - white-space: nowrap; - } - } - } - - .diverged-commits-count { - color: $gl-text-color-secondary; - } -} - .card-new-merge-request { .card-header { padding: 5px 10px; @@ -639,75 +214,6 @@ $tabs-holder-z-index: 250; } } -.mr-version-controls { - position: relative; - z-index: $tabs-holder-z-index + 10; - background: $white; - color: $gl-text-color; - margin-top: -1px; - - .mr-version-menus-container { - display: flex; - align-items: center; - flex-wrap: wrap; - padding: 16px; - z-index: 199; - white-space: nowrap; - - .gl-dropdown-toggle { - width: auto; - max-width: 170px; - - svg { - top: 10px; - right: 8px; - } - } - } - - .content-block { - padding: $gl-padding; - border-bottom: 0; - } - - .mr-version-dropdown, - .mr-version-compare-dropdown { - margin: 0 0.5rem; - } - - .dropdown-title { - color: $gl-text-color; - } - - // Shortening button height by 1px to make compare-versions - // header 56px and fit into our 8px design grid - .btn { - height: 34px; - } - - @include media-breakpoint-up(md) { - position: -webkit-sticky; - position: sticky; - top: calc(#{$header-height} + #{$mr-tabs-height}); - - .with-system-header & { - top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height}); - } - - .with-system-header.with-performance-bar & { - top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height} + #{$performance-bar-height}); - } - - .mr-version-menus-container { - flex-wrap: nowrap; - } - - .with-performance-bar & { - top: calc(#{$header-height} + #{$performance-bar-height} + #{$mr-tabs-height}); - } - } -} - .merge-request-tabs-holder, .epic-tabs-holder { top: $header-height; @@ -833,80 +339,10 @@ $tabs-holder-z-index: 250; } } -.mr-memory-usage { - width: 100%; - - p.usage-info-loading .usage-info-load-spinner { - margin-right: 10px; - font-size: 16px; - } -} - .fork-sprite { margin-right: -5px; } -.deploy-heading, -.merge-train-position-indicator { - @include media-breakpoint-up(md) { - padding: $gl-padding-8 $gl-padding; - } - - .media-body { - min-width: 0; - font-size: 12px; - margin-left: 32px; - } - - &:not(:last-child) { - border-bottom: 1px solid $border-color; - } -} - -.deploy-body { - display: flex; - align-items: center; - flex-wrap: wrap; - - @include media-breakpoint-up(xs) { - flex-wrap: nowrap; - white-space: nowrap; - } - - @include media-breakpoint-down(md) { - flex-direction: column; - align-items: flex-start; - - .deployment-info { - margin-bottom: $gl-padding; - } - } - - > *:not(:last-child) { - margin-right: 0.3em; - } - - svg { - vertical-align: text-top; - } - - .deployment-info { - flex: 1; - white-space: nowrap; - text-overflow: ellipsis; - min-width: 100px; - - @include media-breakpoint-up(xs) { - min-width: 0; - max-width: 100%; - } - } - - .dropdown-menu { - width: 400px; - } -} - // Hack alert: we've rewritten `btn` class in a way that // we've broken it and it is not possible to use with `btn-link` // which causes a blank button when it's disabled and hovering @@ -924,30 +360,6 @@ $tabs-holder-z-index: 250; } } -.ci-widget-container { - justify-content: space-between; - flex: 1; - flex-direction: row; - - @include media-breakpoint-down(sm) { - flex-direction: column; - - .stage-cell .stage-container { - margin-top: 16px; - } - - .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu { - transform: initial; - } - } - - .coverage { - font-size: 12px; - color: $gray-500; - line-height: initial; - } -} - .merge-request-details .file-finder-overlay.diff-file-finder { position: fixed; z-index: 99999; @@ -963,47 +375,3 @@ $tabs-holder-z-index: 250; } } } - -.diff-file-row.is-active { - background-color: $gray-50; -} - -.mr-conflict-loader { - max-width: 334px; - - > svg { - vertical-align: middle; - } -} - -.mr-ready-to-merge-loader { - max-width: 418px; - - > svg { - vertical-align: middle; - } -} - -.mr-widget-alert-container { - $radius: $border-radius-default - 1px; - - border-radius: $radius $radius 0 0; - - .gl-alert:not(:last-child) { - margin-bottom: 1px; - } -} - -.mr-widget-extension-icon::before { - @include gl-content-empty; - @include gl-absolute; - @include gl-left-0; - @include gl-top-0; - @include gl-opacity-3; - @include gl-border-solid; - @include gl-border-4; - @include gl-rounded-full; - - width: 24px; - height: 24px; -} diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c84d6256339..d2e3c7d0272 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -419,6 +419,7 @@ class ProjectsController < Projects::ApplicationController %i[ show_default_award_emojis squash_option + show_diff_preview_in_email mr_default_target_self warn_about_potentially_unwanted_characters ] diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b39cd485fe8..fd0ae40e4dc 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -586,6 +586,7 @@ module ProjectsHelper metricsDashboardAccessLevel: feature.metrics_dashboard_access_level, operationsAccessLevel: feature.operations_access_level, showDefaultAwardEmojis: project.show_default_award_emojis?, + showDiffPreviewInEmail: project.show_diff_preview_in_email?, warnAboutPotentiallyUnwantedCharacters: project.warn_about_potentially_unwanted_characters?, securityAndComplianceAccessLevel: project.security_and_compliance_access_level, containerRegistryAccessLevel: feature.container_registry_access_level diff --git a/app/helpers/users/group_callouts_helper.rb b/app/helpers/users/group_callouts_helper.rb index b66c7f9f821..0aa4eb89499 100644 --- a/app/helpers/users/group_callouts_helper.rb +++ b/app/helpers/users/group_callouts_helper.rb @@ -3,6 +3,7 @@ module Users module GroupCalloutsHelper INVITE_MEMBERS_BANNER = 'invite_members_banner' + APPROACHING_SEAT_COUNT_THRESHOLD = 'approaching_seat_count_threshold' def show_invite_banner?(group) Ability.allowed?(current_user, :admin_group, group) && diff --git a/app/models/deployment.rb b/app/models/deployment.rb index beee1f90198..46409465209 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -294,10 +294,6 @@ class Deployment < ApplicationRecord @stop_action ||= manual_actions.find { |action| action.name == self.on_stop } end - def finished_at - read_attribute(:finished_at) || legacy_finished_at - end - def deployed_at return unless success? @@ -405,10 +401,6 @@ class Deployment < ApplicationRecord raise ArgumentError, "The status #{status.inspect} is invalid" end end - - def legacy_finished_at - self.created_at if success? && !read_attribute(:finished_at) - end end Deployment.prepend_mod_with('Deployment') diff --git a/app/models/project.rb b/app/models/project.rb index abe072a3a6d..2b90f176279 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -433,6 +433,7 @@ class Project < ApplicationRecord alias_method :container_registry_enabled, :container_registry_enabled? delegate :show_default_award_emojis, :show_default_award_emojis=, :show_default_award_emojis?, :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, :warn_about_potentially_unwanted_characters?, + :show_diff_preview_in_email, :show_diff_preview_in_email=, :show_diff_preview_in_email?, to: :project_setting, allow_nil: true delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?, prefix: :import, to: :import_state, allow_nil: true diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb index da9b95fd718..faa5130e6ec 100644 --- a/app/models/users/group_callout.rb +++ b/app/models/users/group_callout.rb @@ -9,7 +9,8 @@ module Users belongs_to :group enum feature_name: { - invite_members_banner: 1 + invite_members_banner: 1, + approaching_seat_count_threshold: 2 # EE-only } validates :group, presence: true diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 4cc5ed06d61..ca7a095b54b 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -229,6 +229,7 @@ class ProjectPolicy < BasePolicy enable :set_note_created_at enable :set_emails_disabled enable :set_show_default_award_emojis + enable :set_show_diff_preview_in_email enable :set_warn_about_potentially_unwanted_characters end diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb index ee0ae6651ca..097b29cf143 100644 --- a/app/services/ci/after_requeue_job_service.rb +++ b/app/services/ci/after_requeue_job_service.rb @@ -22,13 +22,9 @@ module Ci end def dependent_jobs - if ::Feature.enabled?(:ci_order_subsequent_jobs_by_stage, @processable.pipeline.project, default_enabled: :yaml) - stage_dependent_jobs - .or(needs_dependent_jobs.except(:preload)) - .ordered_by_stage - else - stage_dependent_jobs | needs_dependent_jobs - end + stage_dependent_jobs + .or(needs_dependent_jobs.except(:preload)) + .ordered_by_stage end def process(job) diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 20bf7d232ce..eba5e7c6e9b 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -1,40 +1,42 @@ -.blank-state-row +- link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5" + +.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between - if has_start_trial? = render_if_exists "dashboard/projects/blank_state_ee_trial" - = link_to new_project_path, class: "blank-state blank-state-link" do + = link_to new_project_path, class: link_classes do .blank-state-icon = custom_icon("add_new_project", size: 50) - .blank-state-body - %h3.blank-state-title - Create a project - %p.blank-state-text - Projects are where you store your code, access issues, wiki and other features of GitLab. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Create a project') + %p + = _('Projects are where you store your code, access issues, wiki and other features of GitLab.') - if current_user.can_create_group? - = link_to new_group_path, class: "blank-state blank-state-link" do + = link_to new_group_path, class: link_classes do .blank-state-icon = custom_icon("add_new_group", size: 50) - .blank-state-body - %h3.blank-state-title - Create a group - %p.blank-state-text - Groups are a great way to organize projects and people. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Create a group') + %p + = _('Groups are a great way to organize projects and people.') - = link_to new_admin_user_path, class: "blank-state blank-state-link" do + = link_to new_admin_user_path, class: link_classes do .blank-state-icon = custom_icon("add_new_user", size: 50) - .blank-state-body - %h3.blank-state-title - Add people - %p.blank-state-text - Add your team members and others to GitLab. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Add people') + %p + = _('Add your team members and others to GitLab.') - = link_to admin_root_path, class: "blank-state blank-state-link" do + = link_to admin_root_path, class: link_classes do .blank-state-icon = custom_icon("configure_server", size: 50) - .blank-state-body - %h3.blank-state-title - Configure GitLab - %p.blank-state-text - Make adjustments to how your GitLab instance is set up. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Configure GitLab') + %p + = _('Make adjustments to how your GitLab instance is set up.') diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml index 003e6f18b33..e0b8850357e 100644 --- a/app/views/dashboard/projects/_blank_state_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -1,48 +1,49 @@ -.blank-state-row +- link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5" + +.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between - if current_user.can_create_project? - = link_to new_project_path, class: "blank-state blank-state-link" do + = link_to new_project_path, class: link_classes do .blank-state-icon = custom_icon("add_new_project", size: 50) - .blank-state-body - %h3.blank-state-title - Create a project - %p.blank-state-text - Projects are where you store your code, access issues, wiki and other features of GitLab. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Create a project') + %p + = _('Projects are where you store your code, access issues, wiki and other features of GitLab.') - else - .blank-state + .blank-state.gl-display-flex.gl-align-items-center.gl-border-1.gl-border-solid.gl-border-gray-100.gl-rounded-base.gl-mb-5 .blank-state-icon = custom_icon("add_new_project", size: 50) - .blank-state-body - %h3.blank-state-title - Create a project - %p.blank-state-text - If you are added to a project, it will be displayed here. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Create a project') + %p + = _('If you are added to a project, it will be displayed here.') - if current_user.can_create_group? - = link_to new_group_path, class: "blank-state blank-state-link" do + = link_to new_group_path, class: link_classes do .blank-state-icon = custom_icon("add_new_group", size: 50) - .blank-state-body - %h3.blank-state-title - Create a group - %p.blank-state-text - Groups are the best way to manage projects and members. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Create a group') + %p + = _('Groups are the best way to manage projects and members.') - = link_to trending_explore_projects_path, class: "blank-state blank-state-link" do + = link_to trending_explore_projects_path, class: link_classes do .blank-state-icon = custom_icon("globe", size: 50) - .blank-state-body - %h3.blank-state-title - Explore public projects - %p.blank-state-text - Public projects are an easy way to allow - everyone to have read-only access. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Explore public projects') + %p + = _('Public projects are an easy way to allow everyone to have read-only access.') - = link_to "https://docs.gitlab.com/", class: "blank-state blank-state-link" do + = link_to "https://docs.gitlab.com/", class: link_classes do .blank-state-icon = custom_icon("lightbulb", size: 50) - .blank-state-body - %h3.blank-state-title - Learn more about GitLab - %p.blank-state-text - Take a look at the documentation to discover all of GitLab's capabilities. + .blank-state-body.gl-sm-pl-0.gl-pl-6 + %h3.gl-font-size-h2.gl-mt-0 + = _('Learn more about GitLab') + %p + = _('Take a look at the documentation to discover all of GitLab’s capabilities.') diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index b5f5025b581..e72762f2ae5 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,13 +1,10 @@ -.blank-state-parent-container - .section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" } - .container.section-body - .row - .blank-state-welcome.w-100 - %h2.blank-state-welcome-title{ data: { qa_selector: 'welcome_title_content' } } - = _('Welcome to GitLab') - %p.blank-state-text - = _('Faster releases. Better code. Less pain.') - - if current_user.admin? - = render "blank_state_admin_welcome" - - else - = render "blank_state_welcome" +.container + .gl-text-center.gl-pt-6.gl-pb-7 + %h2.gl-font-size-h1{ data: { qa_selector: 'welcome_title_content' } } + = _('Welcome to GitLab') + %p.gl-m-0 + = _('Faster releases. Better code. Less pain.') + - if current_user.admin? + = render "blank_state_admin_welcome" + - else + = render "blank_state_welcome" diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 4252b60514a..0d9257e659a 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -7,6 +7,7 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path +- add_page_specific_style 'page_bundles/dashboard_projects' = render "projects/last_push" - if show_projects?(@projects, params) diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml index ad0c873bf56..1e927b5805d 100644 --- a/app/views/notify/_note_email.html.haml +++ b/app/views/notify/_note_email.html.haml @@ -20,8 +20,7 @@ discussion on #{link_to(discussion.file_path, target_url)} - else = link_to 'discussion', target_url - -- if discussion&.diff_discussion? && discussion.on_text? +- if discussion&.diff_discussion? && discussion.on_text? && @project.show_diff_preview_in_email? = content_for :head do = stylesheet_link_tag 'mailers/highlighted_diff_email' diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb index 8e2f7e6f76e..e212b165134 100644 --- a/app/views/notify/_note_email.text.erb +++ b/app/views/notify/_note_email.text.erb @@ -20,7 +20,7 @@ <% end -%> -<% if discussion&.diff_discussion? && discussion.on_text? -%> +<% if discussion&.diff_discussion? && discussion.on_text? && @project.show_diff_preview_in_email? -%> <% discussion.truncated_diff_lines(highlight: false, diff_limit: diff_limit).each do |line| -%> <%= "> #{line.text}\n" -%> <% end -%> diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index b123b81b89c..6d60ef92d86 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -14,7 +14,7 @@ .text-content %h4.state-title = _("You don't have any deployments right now.") - %p.blank-state-text + %p = html_escape(_("Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } .text-center = link_to _("Read more"), help_page_path("ci/environments/index.md"), class: "gl-button btn btn-confirm" diff --git a/config/application.rb b/config/application.rb index c55841b40b9..8d795e6bc4e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -257,6 +257,7 @@ module Gitlab config.assets.precompile << "page_bundles/build.css" config.assets.precompile << "page_bundles/ci_status.css" config.assets.precompile << "page_bundles/cycle_analytics.css" + config.assets.precompile << "page_bundles/dashboard_projects.css" config.assets.precompile << "page_bundles/dev_ops_reports.css" config.assets.precompile << "page_bundles/environments.css" config.assets.precompile << "page_bundles/epics.css" diff --git a/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml b/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml deleted file mode 100644 index babe10764e3..00000000000 --- a/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_order_subsequent_jobs_by_stage -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77528 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349977 -milestone: '14.7' -type: development -group: group::pipeline authoring -default_enabled: true diff --git a/config/feature_flags/development/redirect_to_latest_template_jobs_build.yml b/config/feature_flags/development/redirect_to_latest_template_jobs_build.yml deleted file mode 100644 index df03505afc5..00000000000 --- a/config/feature_flags/development/redirect_to_latest_template_jobs_build.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: redirect_to_latest_template_jobs_build -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67782 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337990 -milestone: '14.2' -type: development -group: group::configure -default_enabled: false diff --git a/db/migrate/20220131192643_add_show_diff_preview_in_email_to_project_settings.rb b/db/migrate/20220131192643_add_show_diff_preview_in_email_to_project_settings.rb new file mode 100644 index 00000000000..1811bf04ee4 --- /dev/null +++ b/db/migrate/20220131192643_add_show_diff_preview_in_email_to_project_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddShowDiffPreviewInEmailToProjectSettings < Gitlab::Database::Migration[1.0] + enable_lock_retries! + + def change + add_column :project_settings, :show_diff_preview_in_email, :boolean, default: true, null: false + end +end diff --git a/db/post_migrate/20220128155251_remove_dangling_running_builds.rb b/db/post_migrate/20220128155251_remove_dangling_running_builds.rb new file mode 100644 index 00000000000..f86a21ced00 --- /dev/null +++ b/db/post_migrate/20220128155251_remove_dangling_running_builds.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class RemoveDanglingRunningBuilds < Gitlab::Database::Migration[1.0] + BATCH_SIZE = 100 + + disable_ddl_transaction! + + def up + each_batch_range('ci_running_builds', of: BATCH_SIZE) do |min, max| + execute <<~SQL + DELETE FROM ci_running_builds + USING ci_builds + WHERE ci_builds.id = ci_running_builds.build_id + AND ci_builds.status = 'failed' + AND ci_builds.type = 'Ci::Build' + AND ci_running_builds.id BETWEEN #{min} AND #{max} + SQL + end + end + + def down + # no-op + # This migration deletes data and it can not be reversed + end +end diff --git a/db/schema_migrations/20220128155251 b/db/schema_migrations/20220128155251 new file mode 100644 index 00000000000..11fa6807ed5 --- /dev/null +++ b/db/schema_migrations/20220128155251 @@ -0,0 +1 @@ +0d121aeecdd6ace1516c2e9b84fefd47d963c4cbe22a0448728241d83da3742e
\ No newline at end of file diff --git a/db/schema_migrations/20220131192643 b/db/schema_migrations/20220131192643 new file mode 100644 index 00000000000..e947bf0cfbe --- /dev/null +++ b/db/schema_migrations/20220131192643 @@ -0,0 +1 @@ +3b9068f109685dcfa8a0a1fda886cca3909d29cbc280cf70ed9f3d927def12ac
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b1f0a258634..a1cd7a08125 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18696,6 +18696,7 @@ CREATE TABLE project_settings ( merge_commit_template text, has_shimo boolean DEFAULT false NOT NULL, squash_commit_template text, + show_diff_preview_in_email boolean DEFAULT true NOT NULL, CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)), CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)), CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)), diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index eea4964efbe..abf5c46114b 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -240,7 +240,7 @@ You can optionally track progress and verify that all packages migrated successf - `sudo gitlab-rails dbconsole` for Omnibus GitLab instances. - `sudo -u git -H psql -d gitlabhq_production` for source-installed instances. -Verify `objectstg` below (where `store=2`) has count of all packages: +Verify `objectstg` below (where `file_store = '2'`) has count of all packages: ```shell gitlabhq_production=# SELECT count(*) AS total, sum(case when file_store = '1' then 1 else 0 end) AS filesystem, sum(case when file_store = '2' then 1 else 0 end) AS objectstg FROM packages_package_files; diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 5efa557d7eb..d5ca93a0a3b 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.0.0' + AUTO_BUILD_IMAGE_VERSION: 'v1.5.0' build: stage: build @@ -19,6 +19,9 @@ build: export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} fi - /build/build.sh + artifacts: + reports: + dotenv: gl-auto-build-variables.env rules: - if: '$BUILD_DISABLED' when: never diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml index 211adc9bd5b..d5ca93a0a3b 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml @@ -1,7 +1,3 @@ -# WARNING: This latest template is for internal FEATURE-FLAG TESTING ONLY. -# It is not meant to be used with `include:`. -# This template is scheduled for removal when testing is complete: https://gitlab.com/gitlab-org/gitlab/-/issues/337987 - variables: AUTO_BUILD_IMAGE_VERSION: 'v1.5.0' diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb index 35f45c8809f..323f59d3373 100644 --- a/lib/gitlab/template/gitlab_ci_yml_template.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -5,9 +5,7 @@ module Gitlab class GitlabCiYmlTemplate < BaseTemplate BASE_EXCLUDED_PATTERNS = [%r{\.latest\.}].freeze - TEMPLATES_WITH_LATEST_VERSION = { - 'Jobs/Build' => true - }.freeze + TEMPLATES_WITH_LATEST_VERSION = {}.freeze def description "# This file is a template, and might need editing before it works on your project." diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cbc023e5b46..22ba8da3e14 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2200,6 +2200,9 @@ msgstr "" msgid "Add or subtract spent time" msgstr "" +msgid "Add people" +msgstr "" + msgid "Add previously merged commits" msgstr "" @@ -2266,6 +2269,9 @@ msgstr "" msgid "Add webhook" msgstr "" +msgid "Add your team members and others to GitLab." +msgstr "" + msgid "Add/remove" msgstr "" @@ -9099,6 +9105,9 @@ msgstr "" msgid "Configure Dependency Scanning in `.gitlab-ci.yml`, creating this file if it does not already exist" msgstr "" +msgid "Configure GitLab" +msgstr "" + msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" msgstr "" @@ -10089,6 +10098,9 @@ msgstr "" msgid "Create a Mattermost team for this group" msgstr "" +msgid "Create a group" +msgstr "" + msgid "Create a merge request" msgstr "" @@ -10113,6 +10125,9 @@ msgstr "" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" +msgid "Create a project" +msgstr "" + msgid "Create an account using:" msgstr "" @@ -14605,6 +14620,9 @@ msgstr "" msgid "Explore public groups" msgstr "" +msgid "Explore public projects" +msgstr "" + msgid "Explore snippets" msgstr "" @@ -16349,6 +16367,9 @@ msgstr "" msgid "GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security." msgstr "" +msgid "GitLab is free to use. Many features for larger teams are part of our %{link_start}paid products%{link_end}. You can try Ultimate for free without any obligation or payment details." +msgstr "" + msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later." msgstr "" @@ -17459,6 +17480,12 @@ msgstr "" msgid "Groups and subgroups" msgstr "" +msgid "Groups are a great way to organize projects and people." +msgstr "" + +msgid "Groups are the best way to manage projects and members." +msgstr "" + msgid "Groups to synchronize" msgstr "" @@ -18138,6 +18165,9 @@ msgstr "" msgid "If you add %{codeStart}needs%{codeEnd} to jobs in your pipeline you'll be able to view the %{codeStart}needs%{codeEnd} relationships between jobs in this tab as a %{linkStart}Directed Acyclic Graph (DAG)%{linkEnd}." msgstr "" +msgid "If you are added to a project, it will be displayed here." +msgstr "" + msgid "If you did not initiate these sign-in attempts, please reach out to your administrator or enable two-factor authentication (2FA) on your account." msgstr "" @@ -21169,6 +21199,9 @@ msgstr "" msgid "Learn more about Auto DevOps" msgstr "" +msgid "Learn more about GitLab" +msgstr "" + msgid "Learn more about Needs relationships" msgstr "" @@ -21846,6 +21879,9 @@ msgstr "" msgid "Maintenance mode" msgstr "" +msgid "Make adjustments to how your GitLab instance is set up." +msgstr "" + msgid "Make and review changes in the browser with the Web IDE" msgstr "" @@ -28264,6 +28300,12 @@ msgstr "" msgid "ProjectSettings|If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts." msgstr "" +msgid "ProjectSettings|Include diff preview in merge request notification emails" +msgstr "" + +msgid "ProjectSettings|Include the code diff preview on comment threads in merge request notification emails." +msgstr "" + msgid "ProjectSettings|Internal" msgstr "" @@ -28594,6 +28636,9 @@ msgstr "" msgid "Projects are organized into groups" msgstr "" +msgid "Projects are where you store your code, access issues, wiki and other features of GitLab." +msgstr "" + msgid "Projects contributed to" msgstr "" @@ -29245,6 +29290,9 @@ msgstr "" msgid "Public projects Minutes cost factor" msgstr "" +msgid "Public projects are an easy way to allow everyone to have read-only access." +msgstr "" + msgid "Publish to status page" msgstr "" @@ -34242,6 +34290,9 @@ msgstr "" msgid "Start date" msgstr "" +msgid "Start free trial" +msgstr "" + msgid "Start merge train" msgstr "" @@ -35316,6 +35367,9 @@ msgstr "" msgid "TagsPage|protected" msgstr "" +msgid "Take a look at the documentation to discover all of GitLab’s capabilities." +msgstr "" + msgid "Target Branch" msgstr "" @@ -38488,6 +38542,9 @@ msgstr "" msgid "Unlock account" msgstr "" +msgid "Unlock more features with GitLab Ultimate" +msgstr "" + msgid "Unlock the discussion" msgstr "" diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index d88ff5c1aa5..bfd54b9c6da 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -132,6 +132,29 @@ RSpec.describe 'Environment' do end end + context 'with upcoming deployments' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let!(:runnind_deployment_1) { create(:deployment, environment: environment, deployable: build, status: :running) } + let!(:runnind_deployment_2) { create(:deployment, environment: environment, deployable: build, status: :running) } + # Success deployments must have present `finished_at`. We'll backfill in the future. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information. + let!(:success_without_finished_at) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: nil) } + + before do + visit_environment(environment) + end + + # This ordering is unexpected and to be fixed. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information. + it 'shows upcoming deployments in unordered way' do + displayed_ids = find_all('[data-testid="deployment-id"]').map { |e| e.text } + internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| "##{d.iid}" } + expect(displayed_ids).to match_array(internal_ids) + end + end + context 'with related deployable present' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb index a0d44b579a8..0e1e6bacd16 100644 --- a/spec/features/projects/settings/project_settings_spec.rb +++ b/spec/features/projects/settings/project_settings_spec.rb @@ -85,6 +85,36 @@ RSpec.describe 'Projects settings' do end end + context 'show diffs in emails', :js do + it 'does not hide diffs by default' do + visit edit_project_path(project) + + show_diff_preview_in_email_input = find('input[name="project[project_setting_attributes][show_diff_preview_in_email]"]', visible: :hidden) + + expect(show_diff_preview_in_email_input.value).to eq('true') + end + + it 'hides diffs in emails when toggled' do + visit edit_project_path(project) + + show_diff_preview_in_email_input = find('input[name="project[project_setting_attributes][show_diff_preview_in_email]"]', visible: :hidden) + show_diff_preview_in_email_checkbox = find('input[name="project[project_setting_attributes][show_diff_preview_in_email]"][type=checkbox]') + + expect(show_diff_preview_in_email_input.value).to eq('true') + + show_diff_preview_in_email_checkbox.click + + expect(show_diff_preview_in_email_input.value).to eq('false') + + page.within('.sharing-permissions') do + find('[data-testid="project-features-save-button"]').click + end + wait_for_requests + + expect(show_diff_preview_in_email_input.value).to eq('false') + end + end + def expect_toggle_state(state) is_collapsed = state == :collapsed diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 305dce51971..776788da879 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -28,6 +28,7 @@ const defaultProps = { emailsDisabled: false, packagesEnabled: true, showDefaultAwardEmojis: true, + showDiffPreviewInEmail: true, warnAboutPotentiallyUnwantedCharacters: true, }, isGitlabCom: true, @@ -101,6 +102,9 @@ describe('Settings Panel', () => { const findEmailSettings = () => wrapper.find({ ref: 'email-settings' }); const findShowDefaultAwardEmojis = () => wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]'); + + const findShowDiffPreviewInEmail = () => + wrapper.find('input[name="project[project_setting_attributes][show_diff_preview_in_email]"]'); const findWarnAboutPuc = () => wrapper.find( 'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]', @@ -585,6 +589,13 @@ describe('Settings Panel', () => { expect(findShowDefaultAwardEmojis().exists()).toBe(true); }); }); + describe('Hide diffs in email', () => { + it('should show the "Hide Diffs in email" input', () => { + wrapper = mountComponent(); + + expect(findShowDiffPreviewInEmail().exists()).toBe(true); + }); + }); describe('Warn about potentially unwanted characters', () => { it('should have a "Warn about Potentially Unwanted Characters" input', () => { diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js index 1db255106ed..4633602de26 100644 --- a/spec/frontend/persistent_user_callout_spec.js +++ b/spec/frontend/persistent_user_callout_spec.js @@ -10,6 +10,7 @@ jest.mock('~/flash'); describe('PersistentUserCallout', () => { const dismissEndpoint = '/dismiss'; const featureName = 'feature'; + const groupId = '5'; function createFixture() { const fixture = document.createElement('div'); @@ -18,6 +19,7 @@ describe('PersistentUserCallout', () => { class="container" data-dismiss-endpoint="${dismissEndpoint}" data-feature-id="${featureName}" + data-group-id="${groupId}" > <button type="button" class="js-close"></button> </div> @@ -86,7 +88,9 @@ describe('PersistentUserCallout', () => { return waitForPromises().then(() => { expect(persistentUserCallout.container.remove).toHaveBeenCalled(); - expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName })); + expect(mockAxios.history.post[0].data).toBe( + JSON.stringify({ feature_name: featureName, group_id: groupId }), + ); }); }); @@ -191,8 +195,8 @@ describe('PersistentUserCallout', () => { return waitForPromises().then(() => { expect(window.location.assign).toBeCalledWith(href); - expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName })); expect(persistentUserCallout.container.remove).not.toHaveBeenCalled(); + expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName })); }); }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 1d78778c757..0d785dcabca 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -964,6 +964,7 @@ RSpec.describe ProjectsHelper do metricsDashboardAccessLevel: project.project_feature.metrics_dashboard_access_level, operationsAccessLevel: project.project_feature.operations_access_level, showDefaultAwardEmojis: project.show_default_award_emojis?, + showDiffPreviewInEmail: project.show_diff_preview_in_email?, securityAndComplianceAccessLevel: project.security_and_compliance_access_level, containerRegistryAccessLevel: project.project_feature.container_registry_access_level ) diff --git a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb new file mode 100644 index 00000000000..a48464befdf --- /dev/null +++ b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('remove_dangling_running_builds') + +RSpec.describe RemoveDanglingRunningBuilds do + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + let(:runner) { table(:ci_runners).create!(runner_type: 1) } + let(:builds) { table(:ci_builds) } + let(:running_builds) { table(:ci_running_builds) } + + let(:running_build) do + builds.create!( + name: 'test 1', + status: 'running', + project_id: project.id, + type: 'Ci::Build') + end + + let(:failed_build) do + builds.create!( + name: 'test 2', + status: 'failed', + project_id: project.id, + type: 'Ci::Build') + end + + let!(:running_metadata) do + running_builds.create!( + build_id: running_build.id, + project_id: project.id, + runner_id: runner.id, + runner_type: + runner.runner_type) + end + + let!(:failed_metadata) do + running_builds.create!( + build_id: failed_build.id, + project_id: project.id, + runner_id: runner.id, + runner_type: runner.runner_type) + end + + it 'removes failed builds' do + migrate! + + expect(running_metadata.reload).to be_present + expect { failed_metadata.reload } .to raise_error(ActiveRecord::RecordNotFound) + end +end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 299800c56a3..47c246d12cc 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -369,38 +369,6 @@ RSpec.describe Deployment do end end - describe '#finished_at' do - subject { deployment.finished_at } - - context 'when deployment status is created' do - let(:deployment) { create(:deployment) } - - it { is_expected.to be_nil } - end - - context 'when deployment status is success' do - let(:deployment) { create(:deployment, :success) } - - it { is_expected.to eq(deployment.read_attribute(:finished_at)) } - end - - context 'when deployment status is success' do - let(:deployment) { create(:deployment, :success, finished_at: nil) } - - before do - deployment.update_column(:finished_at, nil) - end - - it { is_expected.to eq(deployment.read_attribute(:created_at)) } - end - - context 'when deployment status is running' do - let(:deployment) { create(:deployment, :running) } - - it { is_expected.to be_nil } - end - end - describe '#deployed_at' do subject { deployment.deployed_at } diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 01d2fb18f00..ef47263ad78 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -145,6 +145,7 @@ project_setting: - project_id - push_rule_id - show_default_award_emojis + - show_diff_preview_in_email - updated_at - cve_id_request_enabled - mr_default_target_self diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 26bc6f747e1..7365ad162d2 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -1043,22 +1043,6 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do expect(all_builds_names).to eq(%w[A1 A2 B]) expect(all_builds_statuses).to eq(%w[pending created created]) end - - context 'when the FF ci_order_subsequent_jobs_by_stage is disabled' do - before do - stub_feature_flags(ci_order_subsequent_jobs_by_stage: false) - end - - it 'processes subsequent jobs in an incorrect order when playing first job' do - expect(all_builds_names).to eq(%w[A1 A2 B]) - expect(all_builds_statuses).to eq(%w[manual skipped skipped]) - - play_manual_action('A1') - - expect(all_builds_names).to eq(%w[A1 A2 B]) - expect(all_builds_statuses).to eq(%w[pending created skipped]) - end - end end private diff --git a/workhorse/internal/upload/rewrite.go b/workhorse/internal/upload/rewrite.go index b9324ac8b7b..bbabe840ef5 100644 --- a/workhorse/internal/upload/rewrite.go +++ b/workhorse/internal/upload/rewrite.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "io/ioutil" + "mime" "mime/multipart" "net/http" + "net/textproto" "os" "path/filepath" "strings" @@ -95,7 +97,8 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr return err } - name := p.FormName() + name, filename := parseAndNormalizeContentDisposition(p.Header) + if name == "" { continue } @@ -104,7 +107,7 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr return ErrInjectedClientParam } - if p.FileName() != "" { + if filename != "" { err = rew.handleFilePart(r.Context(), name, p, opts) } else { err = rew.copyPart(r.Context(), name, p) @@ -118,6 +121,13 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr return nil } +func parseAndNormalizeContentDisposition(header textproto.MIMEHeader) (string, string) { + const key = "Content-Disposition" + mediaType, params, _ := mime.ParseMediaType(header.Get(key)) + header.Set(key, mime.FormatMediaType(mediaType, params)) + return params["name"], params["filename"] +} + func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipart.Part, opts *filestore.SaveFileOpts) error { if rew.filter.Count() >= maxFilesAllowed { return ErrTooManyFilesUploaded diff --git a/workhorse/internal/upload/rewrite_test.go b/workhorse/internal/upload/rewrite_test.go index e3f33a02489..145f62ee910 100644 --- a/workhorse/internal/upload/rewrite_test.go +++ b/workhorse/internal/upload/rewrite_test.go @@ -1,6 +1,7 @@ package upload import ( + "net/textproto" "os" "runtime" "testing" @@ -54,3 +55,83 @@ func TestImageTypeRecongition(t *testing.T) { }) } } + +func TestParseAndNormalizeContentDisposition(t *testing.T) { + tests := []struct { + desc string + header string + name string + filename string + sanitizedHeader string + }{ + { + desc: "without content disposition", + header: "", + name: "", + filename: "", + sanitizedHeader: "", + }, { + desc: "content disposition without filename", + header: `form-data; name="filename"`, + name: "filename", + filename: "", + sanitizedHeader: `form-data; name=filename`, + }, { + desc: "with filename", + header: `form-data; name="file"; filename=foobar`, + name: "file", + filename: "foobar", + sanitizedHeader: `form-data; filename=foobar; name=file`, + }, { + desc: "with filename*", + header: `form-data; name="file"; filename*=UTF-8''bar`, + name: "file", + filename: "bar", + sanitizedHeader: `form-data; filename=bar; name=file`, + }, { + desc: "filename and filename*", + header: `form-data; name="file"; filename=foobar; filename*=UTF-8''bar`, + name: "file", + filename: "bar", + sanitizedHeader: `form-data; filename=bar; name=file`, + }, { + desc: "with empty filename", + header: `form-data; name="file"; filename=""`, + name: "file", + filename: "", + sanitizedHeader: `form-data; filename=""; name=file`, + }, { + desc: "with complex filename*", + header: `form-data; name="file"; filename*=UTF-8''viel%20Spa%C3%9F`, + name: "file", + filename: "viel Spaß", + sanitizedHeader: `form-data; filename*=utf-8''viel%20Spa%C3%9F; name=file`, + }, { + desc: "with unsupported charset", + header: `form-data; name="file"; filename*=UTF-16''bar`, + name: "file", + filename: "", + sanitizedHeader: `form-data; name=file`, + }, { + desc: "with filename and filename* with unsupported charset", + header: `form-data; name="file"; filename=foobar; filename*=UTF-16''bar`, + name: "file", + filename: "foobar", + sanitizedHeader: `form-data; filename=foobar; name=file`, + }, + } + + for _, testCase := range tests { + t.Run(testCase.desc, func(t *testing.T) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", testCase.header) + h.Set("Content-Type", "application/octet-stream") + + name, filename := parseAndNormalizeContentDisposition(h) + + require.Equal(t, testCase.name, name) + require.Equal(t, testCase.filename, filename) + require.Equal(t, testCase.sanitizedHeader, h.Get("Content-Disposition")) + }) + } +} diff --git a/workhorse/internal/upload/uploads_test.go b/workhorse/internal/upload/uploads_test.go index 7c22b3b4e28..31fd3126dd4 100644 --- a/workhorse/internal/upload/uploads_test.go +++ b/workhorse/internal/upload/uploads_test.go @@ -1,13 +1,16 @@ package upload import ( + "bufio" "bytes" "context" "fmt" + "io" "io/ioutil" "mime/multipart" "net/http" "net/http/httptest" + "net/textproto" "os" "regexp" "strconv" @@ -384,6 +387,89 @@ func TestInvalidFileNames(t *testing.T) { } } +func TestContentDispositionRewrite(t *testing.T) { + testhelper.ConfigureSecret() + + tempPath, err := ioutil.TempDir("", "uploads") + require.NoError(t, err) + defer os.RemoveAll(tempPath) + + tests := []struct { + desc string + header string + code int + sanitizedHeader string + }{ + { + desc: "with name", + header: `form-data; name="foo"`, + code: 200, + sanitizedHeader: `form-data; name=foo`, + }, + { + desc: "with name and name*", + header: `form-data; name="foo"; name*=UTF-8''bar`, + code: 200, + sanitizedHeader: `form-data; name=bar`, + }, + { + desc: "with name and invalid name*", + header: `form-data; name="foo"; name*=UTF-16''bar`, + code: 200, + sanitizedHeader: `form-data; name=foo`, + }, + } + + for _, testCase := range tests { + t.Run(testCase.desc, func(t *testing.T) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", testCase.header) + h.Set("Content-Type", "application/octet-stream") + + buffer := &bytes.Buffer{} + writer := multipart.NewWriter(buffer) + file, err := writer.CreatePart(h) + require.NoError(t, err) + fmt.Fprint(file, "test") + writer.Close() + + httpRequest := httptest.NewRequest("POST", "/example", buffer) + httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) + + var upstreamRequestBuffer bytes.Buffer + customHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + r.Write(&upstreamRequestBuffer) + }) + + response := httptest.NewRecorder() + apiResponse := &api.Response{TempPath: tempPath} + preparer := &DefaultPreparer{} + opts, _, err := preparer.Prepare(apiResponse) + require.NoError(t, err) + + HandleFileUploads(response, httpRequest, customHandler, apiResponse, &SavedFileTracker{Request: httpRequest}, opts) + + upstreamRequest, err := http.ReadRequest(bufio.NewReader(&upstreamRequestBuffer)) + require.NoError(t, err) + + reader, err := upstreamRequest.MultipartReader() + require.NoError(t, err) + + for i := 0; ; i++ { + p, err := reader.NextPart() + if err == io.EOF { + require.Equal(t, i, 1) + break + } + require.NoError(t, err) + require.Equal(t, testCase.sanitizedHeader, p.Header.Get("Content-Disposition")) + } + + require.Equal(t, testCase.code, response.Code) + }) + } +} + func TestUploadHandlerRemovingExif(t *testing.T) { content, err := ioutil.ReadFile("exif/testdata/sample_exif.jpg") require.NoError(t, err) |