diff options
61 files changed, 1440 insertions, 322 deletions
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue index bee93e434d6..d2fb524489e 100644 --- a/app/assets/javascripts/projects/compare/components/app.vue +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -1,12 +1,12 @@ <script> import { GlButton } from '@gitlab/ui'; import csrf from '~/lib/utils/csrf'; -import RevisionDropdown from './revision_dropdown.vue'; +import RevisionCard from './revision_card.vue'; export default { csrf, components: { - RevisionDropdown, + RevisionCard, GlButton, }, props: { @@ -48,42 +48,53 @@ export default { <template> <form ref="form" - class="form-inline js-requires-input js-signature-container" + class="js-requires-input js-signature-container" method="POST" :action="projectCompareIndexPath" > <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> - <revision-dropdown - :refs-project-path="refsProjectPath" - revision-text="Source" - params-name="to" - :params-branch="paramsTo" - /> - <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div> - <revision-dropdown - :refs-project-path="refsProjectPath" - revision-text="Target" - params-name="from" - :params-branch="paramsFrom" - /> - <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit"> - {{ s__('CompareRevisions|Compare') }} - </gl-button> - <gl-button - v-if="projectMergeRequestPath" - :href="projectMergeRequestPath" - data-testid="projectMrButton" - class="btn btn-default gl-button gl-ml-3" + <div + class="gl-lg-flex-direction-row gl-lg-display-flex gl-align-items-center compare-revision-cards" > - {{ s__('CompareRevisions|View open merge request') }} - </gl-button> - <gl-button - v-else-if="createMrPath" - :href="createMrPath" - data-testid="createMrButton" - class="btn btn-default gl-button gl-ml-3" - > - {{ s__('CompareRevisions|Create merge request') }} - </gl-button> + <revision-card + :refs-project-path="refsProjectPath" + revision-text="Source" + params-name="to" + :params-branch="paramsTo" + /> + <div + class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0" + data-testid="ellipsis" + > + ... + </div> + <revision-card + :refs-project-path="refsProjectPath" + revision-text="Target" + params-name="from" + :params-branch="paramsFrom" + /> + </div> + <div class="gl-mt-4"> + <gl-button category="primary" variant="success" @click="onSubmit"> + {{ s__('CompareRevisions|Compare') }} + </gl-button> + <gl-button + v-if="projectMergeRequestPath" + :href="projectMergeRequestPath" + data-testid="projectMrButton" + class="btn btn-default gl-button" + > + {{ s__('CompareRevisions|View open merge request') }} + </gl-button> + <gl-button + v-else-if="createMrPath" + :href="createMrPath" + data-testid="createMrButton" + class="btn btn-default gl-button" + > + {{ s__('CompareRevisions|Create merge request') }} + </gl-button> + </div> </form> </template> diff --git a/app/assets/javascripts/projects/compare/components/app_legacy.vue b/app/assets/javascripts/projects/compare/components/app_legacy.vue new file mode 100644 index 00000000000..c0ff58ee074 --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/app_legacy.vue @@ -0,0 +1,89 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import RevisionDropdown from './revision_dropdown_legacy.vue'; + +export default { + csrf, + components: { + RevisionDropdown, + GlButton, + }, + props: { + projectCompareIndexPath: { + type: String, + required: true, + }, + refsProjectPath: { + type: String, + required: true, + }, + paramsFrom: { + type: String, + required: false, + default: null, + }, + paramsTo: { + type: String, + required: false, + default: null, + }, + projectMergeRequestPath: { + type: String, + required: true, + }, + createMrPath: { + type: String, + required: true, + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <form + ref="form" + class="form-inline js-requires-input js-signature-container" + method="POST" + :action="projectCompareIndexPath" + > + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Source" + params-name="to" + :params-branch="paramsTo" + /> + <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Target" + params-name="from" + :params-branch="paramsFrom" + /> + <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit"> + {{ s__('CompareRevisions|Compare') }} + </gl-button> + <gl-button + v-if="projectMergeRequestPath" + :href="projectMergeRequestPath" + data-testid="projectMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|View open merge request') }} + </gl-button> + <gl-button + v-else-if="createMrPath" + :href="createMrPath" + data-testid="createMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|Create merge request') }} + </gl-button> + </form> +</template> diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue new file mode 100644 index 00000000000..822dfc09d81 --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue @@ -0,0 +1,93 @@ +<script> +import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; + +const SOURCE_PARAM_NAME = 'to'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlSearchBoxByType, + }, + inject: ['projectTo', 'projectsFrom'], + props: { + paramsName: { + type: String, + required: true, + }, + }, + data() { + return { + searchTerm: '', + selectedRepo: {}, + }; + }, + computed: { + filteredRepos() { + const lowerCaseSearchTerm = this.searchTerm.toLowerCase(); + + return this?.projectsFrom.filter(({ name }) => + name.toLowerCase().includes(lowerCaseSearchTerm), + ); + }, + isSourceRevision() { + return this.paramsName === SOURCE_PARAM_NAME; + }, + inputName() { + return `${this.paramsName}_project_id`; + }, + }, + mounted() { + this.setDefaultRepo(); + }, + methods: { + onClick(repo) { + this.selectedRepo = repo; + this.emitTargetProject(repo.name); + }, + setDefaultRepo() { + if (this.isSourceRevision) { + this.selectedRepo = this.projectTo; + return; + } + + const [defaultTargetProject] = this.projectsFrom; + this.emitTargetProject(defaultTargetProject.name); + this.selectedRepo = defaultTargetProject; + }, + emitTargetProject(name) { + if (!this.isSourceRevision) { + this.$emit('changeTargetProject', name); + } + }, + }, +}; +</script> + +<template> + <div> + <input type="hidden" :name="inputName" :value="selectedRepo.id" /> + <gl-dropdown + :text="selectedRepo.name" + :header-text="s__(`CompareRevisions|Select target project`)" + class="gl-w-full gl-font-monospace gl-sm-pr-3" + toggle-class="gl-min-w-0" + :disabled="isSourceRevision" + > + <template #header> + <gl-search-box-by-type v-if="!isSourceRevision" v-model.trim="searchTerm" /> + </template> + <template v-if="!isSourceRevision"> + <gl-dropdown-item + v-for="repo in filteredRepos" + :key="repo.id" + is-check-item + :is-checked="selectedRepo.id === repo.id" + @click="onClick(repo)" + > + {{ repo.name }} + </gl-dropdown-item> + </template> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue new file mode 100644 index 00000000000..15d24792310 --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/revision_card.vue @@ -0,0 +1,65 @@ +<script> +import { GlCard } from '@gitlab/ui'; +import RepoDropdown from './repo_dropdown.vue'; +import RevisionDropdown from './revision_dropdown.vue'; + +export default { + components: { + RepoDropdown, + RevisionDropdown, + GlCard, + }, + props: { + refsProjectPath: { + type: String, + required: true, + }, + revisionText: { + type: String, + required: true, + }, + paramsName: { + type: String, + required: true, + }, + paramsBranch: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + selectedRefsProjectPath: this.refsProjectPath, + }; + }, + methods: { + onChangeTargetProject(targetProjectName) { + if (this.paramsName === 'from') { + this.selectedRefsProjectPath = `/${targetProjectName}/refs`; + } + }, + }, +}; +</script> + +<template> + <gl-card header-class="gl-py-2 gl-px-3 gl-font-weight-bold" body-class="gl-px-3"> + <template #header> + {{ s__(`CompareRevisions|${revisionText}`) }} + </template> + <div class="gl-sm-display-flex gl-align-items-center"> + <repo-dropdown + class="gl-sm-w-half" + :params-name="paramsName" + @changeTargetProject="onChangeTargetProject" + /> + <revision-dropdown + class="gl-sm-w-half gl-mt-3 gl-sm-mt-0" + :refs-project-path="selectedRefsProjectPath" + :params-name="paramsName" + :params-branch="paramsBranch" + /> + </div> + </gl-card> +</template> diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue index 13d80b5ae0b..d8947d60a8e 100644 --- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue @@ -4,6 +4,8 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; +const emptyDropdownText = s__('CompareRevisions|Select branch/tag'); + export default { components: { GlDropdown, @@ -16,10 +18,6 @@ export default { type: String, required: true, }, - revisionText: { - type: String, - required: true, - }, paramsName: { type: String, required: true, @@ -55,12 +53,24 @@ export default { return this.filteredTags.length; }, }, + watch: { + refsProjectPath(newRefsProjectPath, oldRefsProjectPath) { + if (newRefsProjectPath !== oldRefsProjectPath) { + this.fetchBranchesAndTags(true); + } + }, + }, mounted() { this.fetchBranchesAndTags(); }, methods: { - fetchBranchesAndTags() { + fetchBranchesAndTags(reset = false) { const endpoint = this.refsProjectPath; + this.loading = true; + + if (reset) { + this.selectedRevision = emptyDropdownText; + } return axios .get(endpoint) @@ -70,9 +80,9 @@ export default { }) .catch(() => { createFlash({ - message: `${s__( - 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.', - )}`, + message: s__( + 'CompareRevisions|There was an error while loading the branch/tag list. Please try again.', + ), }); }) .finally(() => { @@ -80,7 +90,7 @@ export default { }); }, getDefaultBranch() { - return this.paramsBranch || s__('CompareRevisions|Select branch/tag'); + return this.paramsBranch || emptyDropdownText; }, onClick(revision) { this.selectedRevision = revision; @@ -93,53 +103,46 @@ export default { </script> <template> - <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`"> - <div class="input-group inline-input-group"> - <span class="input-group-prepend"> - <div class="input-group-text"> - {{ revisionText }} - </div> - </span> - <input type="hidden" :name="paramsName" :value="selectedRevision" /> - <gl-dropdown - class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace" - toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!" - :text="selectedRevision" - header-text="Select Git revision" - :loading="loading" + <div :class="`js-compare-${paramsName}-dropdown`"> + <input type="hidden" :name="paramsName" :value="selectedRevision" /> + <gl-dropdown + class="gl-w-full gl-font-monospace" + toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0" + :text="selectedRevision" + :header-text="s__('CompareRevisions|Select Git revision')" + :loading="loading" + > + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :placeholder="s__('CompareRevisions|Filter by Git revision')" + @keyup.enter="onSearchEnter" + /> + </template> + <gl-dropdown-section-header v-if="hasFilteredBranches"> + {{ s__('CompareRevisions|Branches') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(branch, index) in filteredBranches" + :key="`branch${index}`" + is-check-item + :is-checked="selectedRevision === branch" + @click="onClick(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-section-header v-if="hasFilteredTags"> + {{ s__('CompareRevisions|Tags') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(tag, index) in filteredTags" + :key="`tag${index}`" + is-check-item + :is-checked="selectedRevision === tag" + @click="onClick(tag)" > - <template #header> - <gl-search-box-by-type - v-model.trim="searchTerm" - :placeholder="s__('CompareRevisions|Filter by Git revision')" - @keyup.enter="onSearchEnter" - /> - </template> - <gl-dropdown-section-header v-if="hasFilteredBranches"> - {{ s__('CompareRevisions|Branches') }} - </gl-dropdown-section-header> - <gl-dropdown-item - v-for="(branch, index) in filteredBranches" - :key="`branch${index}`" - is-check-item - :is-checked="selectedRevision === branch" - @click="onClick(branch)" - > - {{ branch }} - </gl-dropdown-item> - <gl-dropdown-section-header v-if="hasFilteredTags"> - {{ s__('CompareRevisions|Tags') }} - </gl-dropdown-section-header> - <gl-dropdown-item - v-for="(tag, index) in filteredTags" - :key="`tag${index}`" - is-check-item - :is-checked="selectedRevision === tag" - @click="onClick(tag)" - > - {{ tag }} - </gl-dropdown-item> - </gl-dropdown> - </div> + {{ tag }} + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue new file mode 100644 index 00000000000..a5e2c986b12 --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue @@ -0,0 +1,145 @@ +<script> +import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + }, + props: { + refsProjectPath: { + type: String, + required: true, + }, + revisionText: { + type: String, + required: true, + }, + paramsName: { + type: String, + required: true, + }, + paramsBranch: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + branches: [], + tags: [], + loading: true, + searchTerm: '', + selectedRevision: this.getDefaultBranch(), + }; + }, + computed: { + filteredBranches() { + return this.branches.filter((branch) => + branch.toLowerCase().includes(this.searchTerm.toLowerCase()), + ); + }, + hasFilteredBranches() { + return this.filteredBranches.length; + }, + filteredTags() { + return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase())); + }, + hasFilteredTags() { + return this.filteredTags.length; + }, + }, + mounted() { + this.fetchBranchesAndTags(); + }, + methods: { + fetchBranchesAndTags() { + const endpoint = this.refsProjectPath; + + return axios + .get(endpoint) + .then(({ data }) => { + this.branches = data.Branches; + this.tags = data.Tags; + }) + .catch(() => { + createFlash({ + message: `${s__( + 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.', + )}`, + }); + }) + .finally(() => { + this.loading = false; + }); + }, + getDefaultBranch() { + return this.paramsBranch || s__('CompareRevisions|Select branch/tag'); + }, + onClick(revision) { + this.selectedRevision = revision; + }, + onSearchEnter() { + this.selectedRevision = this.searchTerm; + }, + }, +}; +</script> + +<template> + <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`"> + <div class="input-group inline-input-group"> + <span class="input-group-prepend"> + <div class="input-group-text"> + {{ revisionText }} + </div> + </span> + <input type="hidden" :name="paramsName" :value="selectedRevision" /> + <gl-dropdown + class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace" + toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!" + :text="selectedRevision" + header-text="Select Git revision" + :loading="loading" + > + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :placeholder="s__('CompareRevisions|Filter by Git revision')" + @keyup.enter="onSearchEnter" + /> + </template> + <gl-dropdown-section-header v-if="hasFilteredBranches"> + {{ s__('CompareRevisions|Branches') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(branch, index) in filteredBranches" + :key="`branch${index}`" + is-check-item + :is-checked="selectedRevision === branch" + @click="onClick(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-section-header v-if="hasFilteredTags"> + {{ s__('CompareRevisions|Tags') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(tag, index) in filteredTags" + :key="`tag${index}`" + is-check-item + :is-checked="selectedRevision === tag" + @click="onClick(tag)" + > + {{ tag }} + </gl-dropdown-item> + </gl-dropdown> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js index 4337eecb667..4ba4e308cd4 100644 --- a/app/assets/javascripts/projects/compare/index.js +++ b/app/assets/javascripts/projects/compare/index.js @@ -1,8 +1,46 @@ import Vue from 'vue'; import CompareApp from './components/app.vue'; +import CompareAppLegacy from './components/app_legacy.vue'; export default function init() { const el = document.getElementById('js-compare-selector'); + + if (gon.features?.compareRepoDropdown) { + const { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + projectTo, + projectsFrom, + } = el.dataset; + + return new Vue({ + el, + components: { + CompareApp, + }, + provide: { + projectTo: JSON.parse(projectTo), + projectsFrom: JSON.parse(projectsFrom), + }, + render(createElement) { + return createElement(CompareApp, { + props: { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + }, + }); + }, + }); + } + const { refsProjectPath, paramsFrom, @@ -15,10 +53,10 @@ export default function init() { return new Vue({ el, components: { - CompareApp, + CompareAppLegacy, }, render(createElement) { - return createElement(CompareApp, { + return createElement(CompareAppLegacy, { props: { refsProjectPath, paramsFrom, diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index 39d9a6a4239..ece4e271200 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -162,6 +162,7 @@ export default { <gl-form-select id="service-desk-template-select" v-model="selectedTemplate" + data-qa-selector="service_desk_template_dropdown" :options="templateOptions" /> <label for="service-desk-email-from-name" class="mt-3"> @@ -175,6 +176,7 @@ export default { <gl-button variant="success" class="gl-mt-5" + data-qa-selector="save_service_desk_settings_button" :disabled="isTemplateSaving" @click="onSaveTemplate" > diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8251cdb9bbb..be857727cf7 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1008,6 +1008,18 @@ pre.light-well { } } +.compare-revision-cards { + @media (min-width: $breakpoint-lg) { + .gl-card { + width: calc(50% - 15px); + } + + .compare-ellipsis { + width: 30px; + } + } +} + .clearable-input { position: relative; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index d424dcbf8f2..d49b58df69b 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -157,3 +157,17 @@ margin-bottom: $gl-spacing-scale-4 !important; } } + +// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168 +.gl-sm-pr-3 { + @media (min-width: $breakpoint-sm) { + padding-right: $gl-spacing-scale-3; + } +} + +// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168 +.gl-sm-w-half { + @media (min-width: $breakpoint-sm) { + width: 50%; + } +} diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 133055cc317..eecd338b7cc 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -20,6 +20,10 @@ class Projects::CompareController < Projects::ApplicationController # Validation before_action :validate_refs! + before_action do + push_frontend_feature_flag(:compare_repo_dropdown) + end + feature_category :source_code_management def index diff --git a/app/services/repositories/changelog_service.rb b/app/services/repositories/changelog_service.rb index 96a63865a49..cead0b00a44 100644 --- a/app/services/repositories/changelog_service.rb +++ b/app/services/repositories/changelog_service.rb @@ -39,10 +39,10 @@ module Repositories project, user, version:, - to:, + branch: project.default_branch_or_master, from: nil, + to: branch, date: DateTime.now, - branch: project.default_branch_or_master, trailer: DEFAULT_TRAILER, file: DEFAULT_FILE, message: "Add changelog for version #{version}" diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml index 676a8a8111a..257944f5e65 100644 --- a/app/views/clusters/clusters/_empty_state.html.haml +++ b/app/views/clusters/clusters/_empty_state.html.haml @@ -11,4 +11,4 @@ - if clusterable.can_add_cluster? .gl-text-center - = link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-success' + = link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-success', data: { qa_selector: 'add_kubernetes_cluster_link' } diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 0998cd480d3..53b9e7f3d65 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -1,5 +1,5 @@ - expanded = expanded_by_default? -%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) } +%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk') %button.btn.gl-button.js-settings-toggle diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 3f9aa24a569..39d4a3b2eb2 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -13,8 +13,17 @@ = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } .prepend-top-20 - #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), - refs_project_path: refs_project_path(@project), - params_from: params[:from], params_to: params[:to], - project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', - create_mr_path: create_mr_button? ? create_mr_path : '' } } + - if Feature.enabled?(:compare_repo_dropdown) + #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), + refs_project_path: refs_project_path(@project), + params_from: params[:from], params_to: params[:to], + project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', + create_mr_path: create_mr_button? ? create_mr_path : '', + project_to: { id: @project.id, name: @project.full_path }.to_json, + projects_from: target_projects(@project).map { |project| { id:project.id, name: project.full_path } }.to_json } } + - else + #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), + refs_project_path: refs_project_path(@project), + params_from: params[:from], params_to: params[:to], + project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', + create_mr_path: create_mr_button? ? create_mr_path : '' } } diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index a739103641e..4e5e04ba4d4 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -8,7 +8,7 @@ %p= _("You can also star a label to make it a priority label.") .text-center - if can?(current_user, :admin_label, @project) - = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success qa-label-create-new', title: _('New label'), id: 'new_label_link' - = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' + = link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm qa-label-create-new', title: _('New label'), id: 'new_label_link' + = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn gl-button btn-confirm-secondary', title: _('Generate a default set of labels'), id: 'generate_labels_link' - if can?(current_user, :admin_label, @group) - = link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' + = link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link' diff --git a/changelogs/unreleased/230465-fj-add-columns-to-track-group-wiki-storage.yml b/changelogs/unreleased/230465-fj-add-columns-to-track-group-wiki-storage.yml new file mode 100644 index 00000000000..3bce5234ea1 --- /dev/null +++ b/changelogs/unreleased/230465-fj-add-columns-to-track-group-wiki-storage.yml @@ -0,0 +1,5 @@ +--- +title: Add wiki_size and storage_size to NamespaceStatistics +merge_request: 54786 +author: +type: added diff --git a/changelogs/unreleased/changelog-api-to-optional.yml b/changelogs/unreleased/changelog-api-to-optional.yml new file mode 100644 index 00000000000..ca08201c3b9 --- /dev/null +++ b/changelogs/unreleased/changelog-api-to-optional.yml @@ -0,0 +1,5 @@ +--- +title: Make `to` in the changelog API optional +merge_request: 54229 +author: +type: added diff --git a/changelogs/unreleased/cngo-convert-threat-monitoring-environment-picker-to-gl-dropdown.yml b/changelogs/unreleased/cngo-convert-threat-monitoring-environment-picker-to-gl-dropdown.yml new file mode 100644 index 00000000000..18e40d36db6 --- /dev/null +++ b/changelogs/unreleased/cngo-convert-threat-monitoring-environment-picker-to-gl-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Convert Threat Monitoring environment picker to GlDropdown +merge_request: 54309 +author: +type: changed diff --git a/changelogs/unreleased/gl-button-labels.yml b/changelogs/unreleased/gl-button-labels.yml new file mode 100644 index 00000000000..ef25d3a778d --- /dev/null +++ b/changelogs/unreleased/gl-button-labels.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for buttons in empty label placeholder +merge_request: 54760 +author: Yogi (@yo) +type: changed diff --git a/data/whats_new/202102180001_13_09.yml b/data/whats_new/202102180001_13_09.yml new file mode 100644 index 00000000000..98ce478cdb2 --- /dev/null +++ b/data/whats_new/202102180001_13_09.yml @@ -0,0 +1,123 @@ +- title: "Select CI/CD configuration from any job and reuse it" + body: | + Previously, if you wanted to reuse the same configuration in multiple jobs, you had two options: add YAML anchors, which don't work across different configuration files, or use `extends` to reuse an entire section. + + In this release, we've added a new YAML function called `!reference`, which lets you target the exact configuration you want to reuse as part of your CI/CD pipeline, even if it's in another file. + stage: Verify + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: https://docs.gitlab.com/ee/ci/yaml/README.html#reference-tags + image_url: https://about.gitlab.com/images/13_9/reference.png + published_at: 2021-02-22 + release: 13.9 +- title: "GPU and smart scheduling support for GitLab Runner" + body: | + Previously, if you wanted to reuse the same configuration in multiple jobs, you had two options: add YAML anchors, which don't work across different configuration files, or use `extends` to reuse an entire section. + + In this release, we've added a new YAML function called `!reference`, which lets you target the exact configuration you want to reuse as part of your CI/CD pipeline, even if it's in another file. + stage: Verify + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: https://docs.gitlab.com/runner/executors/docker_machine.html#using-gpus-on-google-compute-engine + image_url: https://about.gitlabcom/images/ci/gitlab-ci-cd-logo_2x.png + published_at: 2021-02-22 + release: 13.9 +- title: "Vault JWT (JSON Web Token) supports GitLab environments" + body: | + To simplify integrations with HashiCorp Vault, we've shipped Vault JWT token support. From the launch, you could restrict access based on data in the JWT. This release gives you a new dimension for restricting access to credentials: the environment a job targets. + + This release extends the existing Vault JWT token to support environment-based restrictions too. As the `environment` name could be supplied by the user running the pipeline, we recommend you use the new environment-based restrictions with the already-existing `ref_type` values for maximum security. + stage: Configure + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html + image_url: https://about.gitlab.com/images/icons/get-a-license.svg + published_at: 2021-02-22 + release: 13.9 +- title: "SAST Support for .NET 5" + body: | + Microsoft's [release of .NET 5.0](https://docs.microsoft.com/en-us/dotnet/core/dotnet-five) is the next major release of .NET Core which supports more types of apps and more platforms than .NET Core or .NET Framework. We have updated our .NET SAST analyzer, [Security Code Scan](https://security-code-scan.github.io/) to support this new version which is also now supported with [our SAST language detection](https://docs.gitlab.com/ee/user/application_security/sast/#supported-languages-and-frameworks) allowing GitLab SAST to automatically detect .NET 5 projects. This change was part of a community contribution by [@shaun.burns](https://gitlab.com/shaun.burns). + stage: Secure + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: https://docs.gitlab.com/ee/user/application_security/sast/analyzers.html#analyzers-data + image_url: https://about.gitlab.com/images/solutions/icon_sheild-check.svg + published_at: 2021-02-22 + release: 13.9 +- title: "Create Jira issues from Vulnerabilities" + body: | + Many customers use Jira as their single source of truth for tracking issues and engineering work. When using GitLab for SCM and CI/CD, they can take advantage of our existing integration to pass information from MRs and commits into existing Jira issues. However, until now there has been no way to automatically pass information about vulnerabilities detected by security scanners into Jira. This leaves users who rely on Jira to track work no choice but to manually copy vulnerability information into new Jira issues. + + To address this, we're introducing a new ability to create Jira issues directly from a vulnerability's details. You will see this as a new option in your existing Jira integration settings. Simply enable the new option and select the desired Jira issue type. Available issue types are pulled automatically based on the currently configured Jira target project. Once enabled, all places in your project where you can create GitLab issues from a vulnerability will instead directly create a Jira issue of the configured type. + stage: Secure + self-managed: true + gitlab-com: true + packages: [Ultimate] + url: https://docs.gitlab.com/ee/user/application_security/vulnerabilities/#create-a-gitlab-issue-for-a-vulnerability + image_url: https://about.gitlab.com/images/13_9/jira-vulnerability-integration.png + published_at: 2021-02-22 + release: 13.9 +- title: "Security Alert Dashboard for Container Network Policy Alerts" + body: | + We're pleased to announce the first release of a dashboard for security alerts! Users can now configure Container Network Policies to send alerts to the security alert dashboard. This is especially useful when traffic must be closely monitored but cannot be blocked entirely without negatively impacting the business. You can configure the alerts at **Security & Compliance > Threat Management > Policies**, and view them at **Security & Compliance > Threat Management > Alerts**. + stage: Secure + self-managed: true + gitlab-com: true + packages: [Ultimate] + url: https://docs.gitlab.com/ee/user/application_security/threat_monitoring/#configuring-network-policy-alerts + image_url: https://about.gitlab.com/images/13_9/security_alert_dashboard.png + published_at: 2021-02-22 + release: 13.9 +- title: "Maintenance Mode" + body: | + Systems administrators occasionally perform maintenance operations on their GitLab instance to keep it performing optimally. Administrators want to offer the highest level of access to their users while these operations are taking place. For example, you may want to perform a [failover test to a secondary site](https://docs.gitlab.com/ee/administration/geo/disaster_recovery/planned_failover.html) as part of the company's business continuity plan. Prior to the failover, you want to pause changes for a short period to ensure the secondary is fully synchronized. Until GitLab 13.8, you could [restrict users from logging in](https://docs.gitlab.com/omnibus/maintenance/#restrict-users-from-logging-into-gitlab), but this would block the entire UI and would render GitLab inaccessible to users. + + GitLab 13.9 introduces [maintenance mode](https://docs.gitlab.com/ee/administration/maintenance_mode/index.html), where write operations are disabled at the application level. This means that GitLab is effectively in a read-only state for all non-administrative users (administrators are still able to edit application settings and [background jobs continue](https://docs.gitlab.com/ee/administration/maintenance_mode/index.html#background-jobs)). Regular users are able to log in to GitLab, view the interface and perform other read-only operations, such as `git clone` or `git pull`. Using maintenance mode, systems administrators can perform maintenance operations, such as failing over to a secondary site, with minimal disruption to regular users. + + Note that GitLab already [supports zero-downtime updates](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates) and enabling maintenance mode is not required to keep your instance up-to-date. + stage: Enablement + self-managed: true + gitlab-com: false + packages: [Premium, Ultimate] + url: https://docs.gitlab.com/ee/administration/maintenance_mode/index.html + image_url: https://about.gitlab.com/images/13_9/maintenance_mode.png + published_at: 2021-02-22 + release: 13.9 +- title: "New merge request metric: mean time to merge" + body: | + A new metric, mean time to merge (MTTM), is available in project-level merge request analytics. MTTM is the average time from when a merge request (MR) is created, until the time it is merged. By selecting different dates in the date range selector, you can see how your time to merge MRs changes over time and understand whether you are becoming more efficient at reviewing and merging code. + stage: Manage + self-managed: true + gitlab-com: true + packages: [Premium, Ultimate] + url: https://docs.gitlab.com/ee/user/analytics/merge_request_analytics.html#mean-time-to-merge + image_url: https://about.gitlab.com/images/13_9/mttm.png + published_at: 2021-02-22 + release: 13.9 +- title: "Filter roadmaps by confidentiality" + body: | + When viewing a roadmap, there used to be no way to hide confidential epics from the main view. This could be frustrating if you wanted to share your roadmap with a public audience. We've updated the search bar filter to include confidentiality so you now have another layer in which you can refine your roadmap. + stage: Plan + self-managed: true + gitlab-com: true + packages: [Premium, Ultimate] + url: https://docs.gitlab.com/ee/user/group/roadmap/#sort-and-filter-the-roadmap + image_url: https://about.gitlab.com/images/13_9/roadmap-filter-by-confidentiality.png + published_at: 2021-02-22 + release: 13.9 +- title: "Allow Deploy Keys to push to protected branches" + body: | + When viewing a roadmap, there used to be no way to hide confidential epics from the main view. This could be frustrating if you wanted to share your roadmap with a public audience. We've updated the search bar filter to include confidentiality so you now have another layer in which you can refine your roadmap. + stage: Plan + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: https://docs.gitlab.com/ee/user/project/protected_branches.html#allow-deploy-keys-to-push-to-a-protected-branch + image_url: https://about.gitlab.com/images/13_9/deploy_keys.png + published_at: 2021-02-22 + release: 13.9 + diff --git a/db/migrate/20210222070356_add_storage_size_to_namespace_statistics.rb b/db/migrate/20210222070356_add_storage_size_to_namespace_statistics.rb new file mode 100644 index 00000000000..838c22382c6 --- /dev/null +++ b/db/migrate/20210222070356_add_storage_size_to_namespace_statistics.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddStorageSizeToNamespaceStatistics < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :namespace_statistics, :storage_size, :bigint, default: 0, null: false + end + end + + def down + with_lock_retries do + remove_column :namespace_statistics, :storage_size + end + end +end diff --git a/db/migrate/20210222070413_add_wiki_size_to_namespace_statistics.rb b/db/migrate/20210222070413_add_wiki_size_to_namespace_statistics.rb new file mode 100644 index 00000000000..9e6ced9fd64 --- /dev/null +++ b/db/migrate/20210222070413_add_wiki_size_to_namespace_statistics.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddWikiSizeToNamespaceStatistics < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :namespace_statistics, :wiki_size, :bigint, default: 0, null: false + end + end + + def down + with_lock_retries do + remove_column :namespace_statistics, :wiki_size + end + end +end diff --git a/db/schema_migrations/20210222070356 b/db/schema_migrations/20210222070356 new file mode 100644 index 00000000000..256bb771dde --- /dev/null +++ b/db/schema_migrations/20210222070356 @@ -0,0 +1 @@ +2aa618cdbb6e55c3af1fa144ae3f3bd0d9d5400706db337301d2d165ba789ef0
\ No newline at end of file diff --git a/db/schema_migrations/20210222070413 b/db/schema_migrations/20210222070413 new file mode 100644 index 00000000000..f1ba0c51922 --- /dev/null +++ b/db/schema_migrations/20210222070413 @@ -0,0 +1 @@ +e13856c6f65d86dae9a3268f91189bb90af236c555d2f645c6ba4c600c996cba
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c2f007c3a84..09f1b91dc28 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14375,7 +14375,9 @@ CREATE TABLE namespace_statistics ( id integer NOT NULL, namespace_id integer NOT NULL, shared_runners_seconds integer DEFAULT 0 NOT NULL, - shared_runners_seconds_last_reset timestamp without time zone + shared_runners_seconds_last_reset timestamp without time zone, + storage_size bigint DEFAULT 0 NOT NULL, + wiki_size bigint DEFAULT 0 NOT NULL ); CREATE SEQUENCE namespace_statistics_id_seq diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md index 6f373089bf2..97bd01aefe3 100644 --- a/doc/administration/repository_storage_paths.md +++ b/doc/administration/repository_storage_paths.md @@ -154,6 +154,5 @@ often it is chosen. That is, `(storage weight) / (sum of all weights) * 100 = ch ## Move repositories -To move a repository to a different repository path, use the -[Project repository storage moves](../api/project_repository_storage_moves.md) API. Use +To move a repository to a different repository path, use the same process as [migrating existing repositories to Gitaly Cluster](gitaly/praefect.md#migrate-existing-repositories-to-gitaly-cluster). diff --git a/doc/api/repositories.md b/doc/api/repositories.md index b0b3160e075..4c8d2c4b02c 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -303,7 +303,7 @@ Supported attributes: | :-------- | :------- | :--------- | :---------- | | `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). | | `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. | -| `to` | string | yes | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. | +| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. | | `date` | datetime | no | The date and time of the release, defaults to the current time. | | `branch` | string | no | The branch to commit the changelog changes to, defaults to the project's default branch. | | `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. | diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index e83789efdbf..72603ed94c0 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -32,7 +32,7 @@ can still be successfully merged into the target. When the merge request can't be merged, the pipeline runs against the source branch only. For example, when: - The target branch has changes that conflict with the changes in the source branch. -- The merge request is a [**Draft** merge request](../../../user/project/merge_requests/work_in_progress_merge_requests.md). +- The merge request is a [**Draft** merge request](../../../user/project/merge_requests/drafts.md). In these cases, the pipeline runs as a [pipeline for merge requests](../index.md) and is labeled as `detached`. If these cases no longer exist, new pipelines diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 9e2ecdeeb5e..6a37514316d 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -56,6 +56,12 @@ A few notes on CPU and storage: to any spinning media for Elasticsearch. In testing, nodes that use SSD storage see boosts in both query and indexing performance. +- We've introduced the [`estimate_cluster_size`](#gitlab-advanced-search-rake-tasks) + Rake task to estimate the Advanced Search storage requirements in advance, which +- The [`estimate_cluster_size`](#gitlab-advanced-search-rake-tasks) Rake task estimates the + Advanced Search storage requirements in advance. The Rake task uses total repository size + for the calculation. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221177) in GitLab 13.10. + Keep in mind, these are **minimum requirements** for Elasticsearch. Heavily-used Elasticsearch clusters will likely require considerably more resources. @@ -421,8 +427,9 @@ The following are some available Rake tasks: | [`sudo gitlab-rake gitlab:elastic:index_snippets`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Performs an Elasticsearch import that indexes the snippets data. | | [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Displays which projects are not indexed. | | [`sudo gitlab-rake gitlab:elastic:reindex_cluster`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Schedules a zero-downtime cluster reindexing task. This feature should be used with an index that was created after GitLab 13.0. | -| [`sudo gitlab-rake gitlab:elastic:mark_reindex_failed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)`] | Mark the most recent re-index job as failed. | -| [`sudo gitlab-rake gitlab:elastic:list_pending_migrations`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)`] | List pending migrations. Pending migrations include those that have not yet started, have started but not finished, and those that are halted. | +| [`sudo gitlab-rake gitlab:elastic:mark_reindex_failed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Mark the most recent re-index job as failed. | +| [`sudo gitlab-rake gitlab:elastic:list_pending_migrations`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | List pending migrations. Pending migrations include those that have not yet started, have started but not finished, and those that are halted. | +| [`sudo gitlab-rake gitlab:elastic:estimate_cluster_size`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Get an estimate of cluster size based on the total repository size. | ### Environment variables diff --git a/doc/topics/gitlab_flow.md b/doc/topics/gitlab_flow.md index d02573a0e06..c238951be51 100644 --- a/doc/topics/gitlab_flow.md +++ b/doc/topics/gitlab_flow.md @@ -187,7 +187,7 @@ The name of a branch might be dictated by organizational standards. When you are done or want to discuss the code, open a merge request. A merge request is an online place to discuss the change and review the code. -If you open the merge request but do not assign it to anyone, it is a [draft merge request](../user/project/merge_requests/work_in_progress_merge_requests.md). +If you open the merge request but do not assign it to anyone, it is a [draft merge request](../user/project/merge_requests/drafts.md). These are used to discuss the proposed implementation but are not ready for inclusion in the `master` branch yet. Start the title of the merge request with `[Draft]`, `Draft:` or `(Draft)` to prevent it from being merged before it's ready. diff --git a/doc/user/group/img/access_requests_management.png b/doc/user/group/img/access_requests_management.png Binary files differdeleted file mode 100644 index 7de6a1c0a5e..00000000000 --- a/doc/user/group/img/access_requests_management.png +++ /dev/null diff --git a/doc/user/group/img/add_new_members_v13_7.png b/doc/user/group/img/add_new_members_v13_7.png Binary files differdeleted file mode 100644 index 7e06bdf8fd1..00000000000 --- a/doc/user/group/img/add_new_members_v13_7.png +++ /dev/null diff --git a/doc/user/group/img/create_new_group_info.png b/doc/user/group/img/create_new_group_info.png Binary files differdeleted file mode 100644 index bd724240c37..00000000000 --- a/doc/user/group/img/create_new_group_info.png +++ /dev/null diff --git a/doc/user/group/img/new_group_from_groups.png b/doc/user/group/img/new_group_from_groups.png Binary files differdeleted file mode 100644 index ffafac1b1cd..00000000000 --- a/doc/user/group/img/new_group_from_groups.png +++ /dev/null diff --git a/doc/user/group/img/new_group_from_other_pages.png b/doc/user/group/img/new_group_from_other_pages.png Binary files differdeleted file mode 100644 index f84501d1ff2..00000000000 --- a/doc/user/group/img/new_group_from_other_pages.png +++ /dev/null diff --git a/doc/user/group/img/request_access_button.png b/doc/user/group/img/request_access_button.png Binary files differdeleted file mode 100644 index 4d73990ec7e..00000000000 --- a/doc/user/group/img/request_access_button.png +++ /dev/null diff --git a/doc/user/group/img/withdraw_access_request_button.png b/doc/user/group/img/withdraw_access_request_button.png Binary files differdeleted file mode 100644 index a5fe78eb090..00000000000 --- a/doc/user/group/img/withdraw_access_request_button.png +++ /dev/null diff --git a/doc/user/group/index.md b/doc/user/group/index.md index cdadb73aee5..6b0cd9780c5 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -50,130 +50,97 @@ For example, consider a user named Alex: ## Create a group -NOTE: -For a list of words that can not be used as group names, see -[reserved names](../reserved_names.md). - -To create a new group, either: - -- In the top menu, click **Groups** and then **Your Groups**, and click the green button **New group**. - - ![new group from groups page](img/new_group_from_groups.png) - -- Or, in the top menu, expand the `plus` sign and choose **New group**. +To create a group: - ![new group from elsewhere](img/new_group_from_other_pages.png) - -Add the following information: - -![new group information](img/create_new_group_info.png) - -1. The **Group name** will automatically populate the URL. Optionally, you can change it. - This is the name that displays in group views. - The name can contain only: +1. From the top menu, either: + - Select **Groups > Your Groups**, and on the right, select the **New group** button. + - To the left of the search box, select the plus sign and then **New group**. +1. For the **Group name**, use only: - Alphanumeric characters - Underscores - Dashes and dots - Spaces -1. The **Group URL** is the namespace under which your projects will be hosted. - The URL can contain only: + + For a list of words that cannot be used as group names, see [reserved names](../reserved_names.md). +1. For the **Group URL**, which is used for the [namespace](#namespaces), + use only: - Alphanumeric characters - Underscores - Dashes and dots (it cannot start with dashes or end in a dot) -1. Optionally, you can add a brief description to tell others - what this group is about. -1. Optionally, choose an avatar for your group. 1. Choose the [visibility level](../../public_access/public_access.md). +1. Invite GitLab members or other users to join the group. -For more details on creating groups, watch the video [GitLab Namespaces (users, groups and subgroups)](https://youtu.be/r0sJgjR2f5A). +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For details about groups, watch [GitLab Namespaces (users, groups and subgroups)](https://youtu.be/r0sJgjR2f5A). ## Add users to a group -A benefit of putting multiple projects in one group is that you can -give a user access to all projects in the group with one action. - -Add members to a group by navigating to the group's dashboard and clicking **Members**. +You can give a user access to all projects in a group. -![add members to group](img/add_new_members_v13_7.png) - -Select the [permission level](../permissions.md#permissions), and add the new member. You can also set the expiring date for that user; this is the date on which they will no longer have access to your group. - -Consider a group with two projects: - -- On the **Group Members** page, you can now add a new user to the group. -- Now, because this user is a **Developer** member of the group, they automatically - get **Developer** access to **all projects** within that group. - -To increase the access level of an existing user for a specific project, -add them again as a new member to the project with the desired permission level. +1. From the top menu, select **Groups > Your Groups**. +1. Find your group and select it. +1. From the left sidebar, select **Members**. +1. Fill in the fields. + - The role applies to all projects in the group. [Learn more about permissions](../permissions.md#permissions). + - On the **Access expiration date**, the user can no longer access projects in the group. ## Request access to a group -As a group owner, you can enable or disable the ability for non-members to request access to -your group. Go to the group settings, and click **Allow users to request access**. - -As a user, you can request to be a member of a group, if that setting is enabled. Go to the group for which you'd like to be a member, and click the **Request Access** button on the right -side of your screen. +As a user, you can request to be a member of a group, if an administrator allows it. -![Request access button](img/request_access_button.png) +1. From the top menu, select **Groups > Your Groups**. +1. Find the group and select it. +1. Under the group name, select **Request Access**. -Once access is requested: +As many as ten of the most-recently-active group owners receive an email with your request. +Any group owner can approve or decline the request. -- Up to ten group owners are notified of your request via email. - Email is sent to the most recently active group owners. -- Any group owner can approve or decline your request on the members page. +If you change your mind before your request is approved, select +**Withdraw Access Request**. -![Manage access requests](img/access_requests_management.png) +## Prevent users from requesting access to a group -If you change your mind before your request is approved, just click the -**Withdraw Access Request** button. +As a group owner, you can prevent non-members from requesting access to +your group. -![Withdraw access request button](img/withdraw_access_request_button.png) +1. From the top menu, select **Groups > Your Groups**. +1. Find the group and select it. +1. From the left menu, select **Settings > General**. +1. Expand the **Permissions, LFS, 2FA** section. +1. Clear the **Allow users to request access** checkbox. +1. Select **Save changes**. ## Change the owner of a group -Ownership of a group means at least one of its members has -[Owner permission](../permissions.md#group-members-permissions). Groups must have at -least one owner. - -Changing the owner of a group with only one owner is possible. To change the sole owner -of a group: +You can change the owner of a group. Each group must always have at least one +member with [Owner permission](../permissions.md#group-members-permissions). - As an administrator: - 1. Go to the group's **{users}** **Members** tab. + 1. Go to the group and from the left menu, select **Members**. 1. Give a different member **Owner** permissions. 1. Refresh the page. You can now remove **Owner** permissions from the original owner. - As the current group's owner: - 1. Go to the group's **{users}** **Members** tab. + 1. Go to the group and from the left menu, select **Members**. 1. Give a different member **Owner** permissions. 1. Have the new owner sign in and remove **Owner** permissions from you. ## Remove a member from the group -Only users with permissions of [Owner](../permissions.md#group-members-permissions) can manage -group members. - -You can remove a member from the group if the given member has a direct membership in the group. If -membership is inherited from a parent group, then the member can be removed only from the parent -group itself. - -When removing a member, you can decide whether to unassign the user from all issues and merge -requests they are currently assigned or leave the assignments as they are. +Prerequisites: -- **Unassigning the removed member** from all issues and merge requests might be helpful when a user - is leaving a private group and you wish to revoke their access to any issues and merge requests - they are assigned. -- **Keeping the issues and merge requests assigned** might be helpful for groups that accept public - contributions where a user doesn't have to be a member to be able to contribute to issues and - merge requests. +- You must have [Owner permissions](../permissions.md#group-members-permissions). +- The member must have direct membership in the group. If + membership is inherited from a parent group, then the member can be removed + from the parent group only. To remove a member from a group: -1. In a group, go to **{users}** **Members**. -1. Click the **Delete** **{remove}** button next to a group member you want to remove. - A **Remove member** modal appears. -1. (Optional) Select the **Also unassign this user from related issues and merge requests** checkbox. -1. Click **Remove member**. +1. Go to the group. +1. From the left menu, select **Members**. +1. Next to the member you want to remove, select **Delete**. +1. Optional. On the **Remove member** confirmation box, select the + **Also unassign this user from related issues and merge requests** checkbox. +1. Select **Remove member**. ## Filter and sort members in a group @@ -745,7 +712,7 @@ To enable this feature: 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**. 1. Click **Save changes**. -#### Enabling delayed Project removal **(PREMIUM)** +#### Enabling delayed project removal **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2. diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index 68b421ec73b..6a5816d1275 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -208,7 +208,12 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD. When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available. -- The `@scope` is the root namespace of the GitLab project. It must match exactly, including the case. +- The `@scope` is the root namespace of the GitLab project. To follow npm's convention, it should be + lowercase. However, the GitLab package registry allows for uppercase. Before GitLab 13.10, the + `@scope` had to be a case-sensitive match of the GitLab project's root namespace. This was + problematic because the npm public registry does not allow uppercase letters. GitLab 13.10 relaxes + this requirement and translates uppercase in the GitLab `@scope` to lowercase for npm. For + example, a package `@MyScope/package-name` in GitLab becomes `@myscope/package-name` for npm. - The `package-name` can be whatever you want. For example, if your project is `https://gitlab.example.com/my-org/engineering-group/team-amazing/analytics`, @@ -218,7 +223,8 @@ the root namespace is `my-org`. When you publish a package, it must have `my-org | ---------------------- | ----------------------- | --------- | | `my-org/bar` | `@my-org/bar` | Yes | | `my-org/bar/baz` | `@my-org/baz` | Yes | -| `My-org/Bar/baz` | `@My-org/Baz` | Yes | +| `My-Org/Bar/baz` | `@my-org/Baz` | Yes | +| `My-Org/Bar/baz` | `@My-Org/Baz` | Yes | | `my-org/bar/buz` | `@my-org/anything` | Yes | | `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes | | `gitlab-org/gitlab` | `@foo/bar` | No | @@ -231,8 +237,7 @@ In GitLab, this regex validates all package names from all package managers: This regex allows almost all of the characters that npm allows, with a few exceptions (for example, `~` is not allowed). -The regex also allows for capital letters, while npm does not. Capital letters are needed because the scope must be -identical to the root namespace of the project. +The regex also allows for capital letters, while npm does not. WARNING: When you update the path of a user or group, or transfer a subgroup or project, diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 44bf97bdd42..dadf585829a 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -49,11 +49,13 @@ To enable 2FA: 1. Select **Enable Two-factor Authentication**. 1. **On your device (usually your phone):** 1. Install a compatible application, like: - - [Authenticator](https://mattrubin.me/authenticator/): open source app for iOS devices. - - [andOTP](https://github.com/andOTP/andOTP): feature rich open source app for Android which supports PGP encrypted backups. - - [FreeOTP](https://freeotp.github.io/): open source app for Android. - - [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en): proprietary app for iOS and Android. - - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp): open source app for SailFish OS. + - [Authy](https://authy.com/) + - [Duo Mobile](https://duo.com/product/multi-factor-authentication-mfa/duo-mobile-app) + - [LastPass](https://lastpass.com/auth/) + - [Authenticator](https://mattrubin.me/authenticator/) + - [andOTP](https://github.com/andOTP/andOTP) + - [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en) + - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp) 1. In the application, add a new entry in one of two ways: - Scan the code presented in GitLab with your device's camera to add the entry automatically. diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index c3adce33826..baf08671894 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -249,7 +249,7 @@ Also: ### Create Merge Request -Create a new branch and [**Draft** merge request](../merge_requests/work_in_progress_merge_requests.md) +Create a new branch and [**Draft** merge request](../merge_requests/drafts.md) in one action. The branch is named `issuenumber-title` by default, but you can choose any name, and GitLab verifies that it is not already in use. The merge request inherits the milestone and labels of the issue, and is set to automatically diff --git a/doc/user/project/merge_requests/drafts.md b/doc/user/project/merge_requests/drafts.md new file mode 100644 index 00000000000..522fef0be71 --- /dev/null +++ b/doc/user/project/merge_requests/drafts.md @@ -0,0 +1,95 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: reference, concepts +disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/work_in_progress_merge_requests.html' +--- + +# Draft merge requests **(FREE)** + +If a merge request isn't ready to merge, potentially because of continued development +or open threads, you can prevent it from being accepted before you +[mark it as ready](#mark-merge-requests-as-ready). Flag it as a draft to disable +the **Merge** button until you remove the **Draft** flag: + +![Blocked Merge Button](img/draft_blocked_merge_button_v13_10.png) + +## Mark merge requests as drafts + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32692) in GitLab 13.2, Work-In-Progress (WIP) merge requests were renamed to **Draft**. Support for using **WIP** is scheduled for removal in GitLab 14.0. +> - **Mark as draft** and **Mark as ready** buttons [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227421) in GitLab 13.5. + +There are several ways to flag a merge request as a draft: + +- **Viewing a merge request**: In the top right corner of the merge request, click **Mark as draft**. +- **Creating or editing a merge request**: Add `[Draft]`, `Draft:` or `(Draft)` to + the beginning of the merge request's title, or click **Start the title with Draft:** + below the **Title** field. +- **Commenting in an existing merge request**: Add the `/draft` + [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) + in a comment. This quick action is a toggle, and can be repeated to change the status + again. This quick action discards any other text in the comment. +- **Creating a commit**: Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the + beginning of a commit message targeting the merge request's source branch. This + is not a toggle, and adding this text again in a later commit doesn't mark the + merge request as ready. + +WARNING: +Adding `WIP:` to the start of the merge request's title still marks a merge request +as a draft. This feature is scheduled for removal in GitLab 14.0. Use `Draft:` instead. + +## Mark merge requests as ready + +When a merge request is ready to be merged, you can remove the `Draft` flag in several ways: + +- **Viewing a merge request**: In the top right corner of the merge request, click **Mark as ready**. + Users with [Developer or greater permissions](../../permissions.md) + can also scroll to the bottom of the merge request description and click **Mark as ready**: + + ![Mark as ready](img/draft_blocked_merge_button_v13_10.png) + +- **Editing an existing merge request**: Remove `[Draft]`, `Draft:` or `(Draft)` + from the beginning of the title, or click **Remove the Draft: prefix from the title** + below the **Title** field. +- **Commenting in an existing merge request**: Add the `/draft` + [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) + in a comment in the merge request. This quick action is a toggle, and can be repeated + to change the status back. This quick action discards any other text in the comment. + +## Include or exclude drafts when searching + +When viewing or searching in your project's merge requests list, you can include or exclude +draft merge requests: + +1. In your project, select **Merge Requests** from the left sidebar. +1. In the navigation bar, click **Open**, **Merged**, **Closed**, or **All** to + filter by merge request status. +1. Click the search box to display a list of filters and select **Draft**, or + enter the word `draft`. +1. Select `=`. +1. Select **Yes** to include drafts, or **No** to exclude, and press **Return** + to update the list of merge requests: + + ![Filter draft merge requests](img/filter_draft_merge_requests_v13_10.png) + +## Pipelines for drafts + +When the [pipelines for merged results](../../../ci/merge_request_pipelines/pipelines_for_merged_results/index.md) +feature is enabled, draft merge requests run +[merge request pipelines](../../../ci/merge_request_pipelines/index.md) only. + +To run pipelines for merged results, you must +[mark the merge request as ready](#mark-merge-requests-as-ready). + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/user/project/merge_requests/getting_started.md b/doc/user/project/merge_requests/getting_started.md index dc6c5cdbfe6..61eb742e42f 100644 --- a/doc/user/project/merge_requests/getting_started.md +++ b/doc/user/project/merge_requests/getting_started.md @@ -60,7 +60,7 @@ request's page at the top-right side: - [Close issues automatically](#merge-requests-to-close-issues) when they are merged. - Enable the [delete source branch when merge request is accepted](#deleting-the-source-branch) option to keep your repository clean. - Enable the [squash commits when merge request is accepted](squash_and_merge.md) option to combine all the commits into one before merging, thus keep a clean commit history in your repository. -- Set the merge request as a [**Draft**](work_in_progress_merge_requests.md) to avoid accidental merges before it is ready. +- Set the merge request as a [**Draft**](drafts.md) to avoid accidental merges before it is ready. After you have created the merge request, you can also: diff --git a/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_10.png b/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_10.png Binary files differnew file mode 100644 index 00000000000..3bac9f7fee8 --- /dev/null +++ b/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_10.png diff --git a/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_2.png b/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_2.png Binary files differdeleted file mode 100644 index f7968772500..00000000000 --- a/doc/user/project/merge_requests/img/draft_blocked_merge_button_v13_2.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/filter_draft_merge_requests_v13_10.png b/doc/user/project/merge_requests/img/filter_draft_merge_requests_v13_10.png Binary files differnew file mode 100644 index 00000000000..4458df987d6 --- /dev/null +++ b/doc/user/project/merge_requests/img/filter_draft_merge_requests_v13_10.png diff --git a/doc/user/project/merge_requests/img/filter_wip_merge_requests.png b/doc/user/project/merge_requests/img/filter_wip_merge_requests.png Binary files differdeleted file mode 100644 index 0989b41e2a4..00000000000 --- a/doc/user/project/merge_requests/img/filter_wip_merge_requests.png +++ /dev/null diff --git a/doc/user/project/merge_requests/merge_request_dependencies.md b/doc/user/project/merge_requests/merge_request_dependencies.md index 1949677177e..2e6b7c33f34 100644 --- a/doc/user/project/merge_requests/merge_request_dependencies.md +++ b/doc/user/project/merge_requests/merge_request_dependencies.md @@ -40,8 +40,7 @@ require changes to `awesome-lib`, and so necessitate two merge requests. Merging the `awesome-project` merge request before the `awesome-lib` one would break the `master`branch. -The `awesome-project` merge request could be [marked as -**Draft**](work_in_progress_merge_requests.md), +The `awesome-project` merge request could be [marked as **Draft**](drafts.md), and the reason for the draft stated included in the comments. However, this requires the state of the `awesome-lib` merge request to be manually tracked, and doesn't scale well if the `awesome-project` merge request diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md index 6eef6505b5d..4b854da116e 100644 --- a/doc/user/project/merge_requests/work_in_progress_merge_requests.md +++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md @@ -1,78 +1,8 @@ --- -stage: Create -group: Code Review -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -type: reference, concepts +redirect_to: 'drafts.md' --- -# Draft merge requests **(FREE)** +This document was moved to [another location](drafts.md). -If a merge request is not yet ready to be merged, perhaps due to continued development -or open threads, you can prevent it from being accepted before it's ready by flagging -it as a **Draft**. This disables the **Merge** button, preventing it from -being merged. It stays disabled until the **Draft** flag has been removed. - -![Blocked Merge Button](img/draft_blocked_merge_button_v13_2.png) - -When [pipelines for merged results](../../../ci/merge_request_pipelines/pipelines_for_merged_results/index.md) -is enabled, draft merge requests run [merge request pipelines](../../../ci/merge_request_pipelines/index.md) -only. - -To run pipelines for merged results, you must [remove the draft status](#removing-the-draft-flag-from-a-merge-request). - -## Adding the "Draft" flag to a merge request - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32692) in GitLab 13.2, Work-In-Progress (WIP) merge requests were renamed to **Draft**. Support for using **WIP** is scheduled for removal in GitLab 14.0. -> - **Mark as draft** and **Mark as ready** buttons [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227421) in GitLab 13.5. - -There are several ways to flag a merge request as a Draft: - -- Click the **Mark as draft** button on the top-right corner of the merge request's page. -- Add `[Draft]`, `Draft:` or `(Draft)` to the start of the merge request's title. Clicking on - **Start the title with Draft:**, under the title box, when editing the merge request's - description has the same effect. -- **Deprecated** Add `[WIP]` or `WIP:` to the start of the merge request's title. - **WIP** still works but was deprecated in favor of **Draft**. It is scheduled for removal in the next major version (GitLab 14.0). -- Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) - in a comment in the merge request. This is a toggle, and can be repeated - to change the status back. Note that any other text in the comment is discarded. -- Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the beginning of a commit message targeting the - merge request's source branch. This is not a toggle, and doing it again in another - commit has no effect. - -## Removing the "Draft" flag from a merge request - -Similar to above, when a Merge Request is ready to be merged, you can remove the -`Draft` flag in several ways: - -- Click the **Mark as ready** button on the top-right corner of the merge request's page. -- Remove `[Draft]`, `Draft:` or `(Draft)` from the start of the merge request's title. Clicking on - **Remove the Draft: prefix from the title**, under the title box, when editing the merge - request's description, has the same effect. -- Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) - in a comment in the merge request. This is a toggle, and can be repeated - to change the status back. Note that any other text in the comment is discarded. -- Click on the **Resolve Draft status** button near the bottom of the merge request description, - next to the **Merge** button (see [image above](#draft-merge-requests)). - Must have at least Developer level permissions on the project for the button to - be visible. - -## Including/excluding WIP merge requests when searching - -When viewing/searching the merge requests list, you can choose to include or exclude -WIP merge requests. Add a **WIP** filter in the search box, and choose **Yes** -to include, or **No** to exclude. - -![Filter WIP MRs](img/filter_wip_merge_requests.png) - -<!-- ## Troubleshooting - -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. - -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +<!-- This redirect file can be deleted after <2021-05-19>. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index eaedd53aedb..f6ffeeea829 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -184,7 +184,7 @@ module API type: String, desc: 'The first commit in the range of commits to use for the changelog' - requires :to, + optional :to, type: String, desc: 'The last commit in the range of commits to use for the changelog' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 126111a70d0..c16bc05e827 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7569,12 +7569,21 @@ msgstr "" msgid "CompareRevisions|Filter by Git revision" msgstr "" +msgid "CompareRevisions|Select Git revision" +msgstr "" + msgid "CompareRevisions|Select branch/tag" msgstr "" +msgid "CompareRevisions|Select target project" +msgstr "" + msgid "CompareRevisions|Tags" msgstr "" +msgid "CompareRevisions|There was an error while loading the branch/tag list. Please try again." +msgstr "" + msgid "CompareRevisions|There was an error while updating the branch/tag list. Please try again." msgstr "" diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index 114e3ddd46a..ca41dddaca2 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -7,11 +7,11 @@ module QA module Kubernetes class Index < Page::Base view 'app/views/clusters/clusters/_empty_state.html.haml' do - element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Integrate with a cluster certificate')" # rubocop:disable QA/ElementWithPattern + element :add_kubernetes_cluster_link end def add_kubernetes_cluster - click_on 'Connect cluster with certificate' + click_element :add_kubernetes_cluster_link end def has_cluster?(cluster) diff --git a/spec/frontend/projects/compare/components/app_legacy_spec.js b/spec/frontend/projects/compare/components/app_legacy_spec.js new file mode 100644 index 00000000000..4c7f0d5cccc --- /dev/null +++ b/spec/frontend/projects/compare/components/app_legacy_spec.js @@ -0,0 +1,116 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import CompareApp from '~/projects/compare/components/app_legacy.vue'; +import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +const projectCompareIndexPath = 'some/path'; +const refsProjectPath = 'some/refs/path'; +const paramsFrom = 'master'; +const paramsTo = 'master'; + +describe('CompareApp component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(CompareApp, { + propsData: { + projectCompareIndexPath, + refsProjectPath, + paramsFrom, + paramsTo, + projectMergeRequestPath: '', + createMrPath: '', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + beforeEach(() => { + createComponent(); + }); + + it('renders component with prop', () => { + expect(wrapper.props()).toEqual( + expect.objectContaining({ + projectCompareIndexPath, + refsProjectPath, + paramsFrom, + paramsTo, + }), + ); + }); + + it('contains the correct form attributes', () => { + expect(wrapper.attributes('action')).toBe(projectCompareIndexPath); + expect(wrapper.attributes('method')).toBe('POST'); + }); + + it('has input with csrf token', () => { + expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('has ellipsis', () => { + expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true); + }); + + it('render Source and Target BranchDropdown components', () => { + const branchDropdowns = wrapper.findAll(RevisionDropdown); + + expect(branchDropdowns.length).toBe(2); + expect(branchDropdowns.at(0).props('revisionText')).toBe('Source'); + expect(branchDropdowns.at(1).props('revisionText')).toBe('Target'); + }); + + describe('compare button', () => { + const findCompareButton = () => wrapper.find(GlButton); + + it('renders button', () => { + expect(findCompareButton().exists()).toBe(true); + }); + + it('submits form', () => { + findCompareButton().vm.$emit('click'); + expect(wrapper.find('form').element.submit).toHaveBeenCalled(); + }); + + it('has compare text', () => { + expect(findCompareButton().text()).toBe('Compare'); + }); + }); + + describe('merge request buttons', () => { + const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]'); + const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]'); + + it('does not have merge request buttons', () => { + createComponent(); + expect(findProjectMrButton().exists()).toBe(false); + expect(findCreateMrButton().exists()).toBe(false); + }); + + it('has "View open merge request" button', () => { + createComponent({ + projectMergeRequestPath: 'some/project/merge/request/path', + }); + expect(findProjectMrButton().exists()).toBe(true); + expect(findCreateMrButton().exists()).toBe(false); + }); + + it('has "Create merge request" button', () => { + createComponent({ + createMrPath: 'some/create/create/mr/path', + }); + expect(findProjectMrButton().exists()).toBe(false); + expect(findCreateMrButton().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js index d28a30e93b1..6de06e4373c 100644 --- a/spec/frontend/projects/compare/components/app_spec.js +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -1,7 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import CompareApp from '~/projects/compare/components/app.vue'; -import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; +import RevisionCard from '~/projects/compare/components/revision_card.vue'; jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); @@ -63,11 +63,11 @@ describe('CompareApp component', () => { }); it('render Source and Target BranchDropdown components', () => { - const branchDropdowns = wrapper.findAll(RevisionDropdown); + const revisionCards = wrapper.findAll(RevisionCard); - expect(branchDropdowns.length).toBe(2); - expect(branchDropdowns.at(0).props('revisionText')).toBe('Source'); - expect(branchDropdowns.at(1).props('revisionText')).toBe('Target'); + expect(revisionCards.length).toBe(2); + expect(revisionCards.at(0).props('revisionText')).toBe('Source'); + expect(revisionCards.at(1).props('revisionText')).toBe('Target'); }); describe('compare button', () => { diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js new file mode 100644 index 00000000000..af76632515c --- /dev/null +++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js @@ -0,0 +1,98 @@ +import { GlDropdown } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue'; + +const defaultProps = { + paramsName: 'to', +}; + +const projectToId = '1'; +const projectToName = 'some-to-name'; +const projectFromId = '2'; +const projectFromName = 'some-from-name'; + +const defaultProvide = { + projectTo: { id: projectToId, name: projectToName }, + projectsFrom: [ + { id: projectFromId, name: projectFromName }, + { id: 3, name: 'some-from-another-name' }, + ], +}; + +describe('RepoDropdown component', () => { + let wrapper; + + const createComponent = (props = {}, provide = {}) => { + wrapper = shallowMount(RepoDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + provide: { + ...defaultProvide, + ...provide, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findGlDropdown = () => wrapper.find(GlDropdown); + const findHiddenInput = () => wrapper.find('input[type="hidden"]'); + + describe('Source Revision', () => { + beforeEach(() => { + createComponent(); + }); + + it('set hidden input', () => { + expect(findHiddenInput().attributes('value')).toBe(projectToId); + }); + + it('displays the project name in the disabled dropdown', () => { + expect(findGlDropdown().props('text')).toBe(projectToName); + expect(findGlDropdown().props('disabled')).toBe(true); + }); + + it('does not emit `changeTargetProject` event', async () => { + wrapper.vm.emitTargetProject('foo'); + await wrapper.vm.$nextTick(); + expect(wrapper.emitted('changeTargetProject')).toBeUndefined(); + }); + }); + + describe('Target Revision', () => { + beforeEach(() => { + createComponent({ paramsName: 'from' }); + }); + + it('set hidden input of the first project', () => { + expect(findHiddenInput().attributes('value')).toBe(projectFromId); + }); + + it('displays the first project name initially in the dropdown', () => { + expect(findGlDropdown().props('text')).toBe(projectFromName); + }); + + it('updates the hiddin input value when onClick method is triggered', async () => { + const repoId = '100'; + wrapper.vm.onClick({ id: repoId }); + await wrapper.vm.$nextTick(); + expect(findHiddenInput().attributes('value')).toBe(repoId); + }); + + it('emits initial `changeTargetProject` event with target project', () => { + expect(wrapper.emitted('changeTargetProject')).toEqual([[projectFromName]]); + }); + + it('emits `changeTargetProject` event when another target project is selected', async () => { + const newTargetProject = 'new-from-name'; + wrapper.vm.$emit('changeTargetProject', newTargetProject); + await wrapper.vm.$nextTick(); + expect(wrapper.emitted('changeTargetProject')[1]).toEqual([newTargetProject]); + }); + }); +}); diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js new file mode 100644 index 00000000000..83f858f4454 --- /dev/null +++ b/spec/frontend/projects/compare/components/revision_card_spec.js @@ -0,0 +1,49 @@ +import { GlCard } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue'; +import RevisionCard from '~/projects/compare/components/revision_card.vue'; +import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; + +const defaultProps = { + refsProjectPath: 'some/refs/path', + revisionText: 'Source', + paramsName: 'to', + paramsBranch: 'master', +}; + +describe('RepoDropdown component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(RevisionCard, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlCard, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + beforeEach(() => { + createComponent(); + }); + + it('displays revision text', () => { + expect(wrapper.find(GlCard).text()).toContain(defaultProps.revisionText); + }); + + it('renders RepoDropdown component', () => { + expect(wrapper.findAll(RepoDropdown).exists()).toBe(true); + }); + + it('renders RevisionDropdown component', () => { + expect(wrapper.findAll(RevisionDropdown).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js new file mode 100644 index 00000000000..8ed419a4a61 --- /dev/null +++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js @@ -0,0 +1,92 @@ +import { GlDropdown } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue'; + +const defaultProps = { + refsProjectPath: 'some/refs/path', + revisionText: 'Target', + paramsName: 'from', + paramsBranch: 'master', +}; + +jest.mock('~/flash'); + +describe('RevisionDropdown component', () => { + let wrapper; + let axiosMock; + + const createComponent = (props = {}) => { + wrapper = shallowMount(RevisionDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + axiosMock.restore(); + }); + + const findGlDropdown = () => wrapper.find(GlDropdown); + + it('sets hidden input', () => { + createComponent(); + expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe( + defaultProps.paramsBranch, + ); + }); + + it('update the branches on success', async () => { + const Branches = ['branch-1', 'branch-2']; + const Tags = ['tag-1', 'tag-2', 'tag-3']; + + axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, { + Branches, + Tags, + }); + + createComponent(); + + await axios.waitForAll(); + + expect(wrapper.vm.branches).toEqual(Branches); + expect(wrapper.vm.tags).toEqual(Tags); + }); + + it('shows flash message on error', async () => { + axiosMock.onGet('some/invalid/path').replyOnce(404); + + createComponent(); + + await wrapper.vm.fetchBranchesAndTags(); + expect(createFlash).toHaveBeenCalled(); + }); + + describe('GlDropdown component', () => { + it('renders props', () => { + createComponent(); + expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps)); + }); + + it('display default text', () => { + createComponent({ + paramsBranch: null, + }); + expect(findGlDropdown().props('text')).toBe('Select branch/tag'); + }); + + it('display params branch text', () => { + createComponent(); + expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch); + }); + }); +}); diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js index f3ff5e26d2b..69d3167c99c 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js @@ -7,7 +7,6 @@ import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vu const defaultProps = { refsProjectPath: 'some/refs/path', - revisionText: 'Target', paramsName: 'from', paramsBranch: 'master', }; @@ -57,7 +56,6 @@ describe('RevisionDropdown component', () => { createComponent(); await axios.waitForAll(); - expect(wrapper.vm.branches).toEqual(Branches); expect(wrapper.vm.tags).toEqual(Tags); }); @@ -71,6 +69,22 @@ describe('RevisionDropdown component', () => { expect(createFlash).toHaveBeenCalled(); }); + it('makes a new request when refsProjectPath is changed', async () => { + jest.spyOn(axios, 'get'); + + const newRefsProjectPath = 'new-selected-project-path'; + + createComponent(); + + wrapper.setProps({ + ...defaultProps, + refsProjectPath: newRefsProjectPath, + }); + + await axios.waitForAll(); + expect(axios.get).toHaveBeenLastCalledWith(newRefsProjectPath); + }); + describe('GlDropdown component', () => { it('renders props', () => { createComponent(); diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index ace73e49c7c..31f0d7cec2a 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -650,6 +650,40 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:ok) end + it 'supports leaving out the from and to attribute' do + spy = instance_spy(Repositories::ChangelogService) + + allow(Repositories::ChangelogService) + .to receive(:new) + .with( + project, + user, + version: '1.0.0', + date: DateTime.new(2020, 1, 1), + branch: 'kittens', + trailer: 'Foo', + file: 'FOO.md', + message: 'Commit message' + ) + .and_return(spy) + + expect(spy).to receive(:execute) + + post( + api("/projects/#{project.id}/repository/changelog", user), + params: { + version: '1.0.0', + date: '2020-01-01', + branch: 'kittens', + trailer: 'Foo', + file: 'FOO.md', + message: 'Commit message' + } + ) + + expect(response).to have_gitlab_http_status(:ok) + end + it 'produces an error when generating the changelog fails' do spy = instance_spy(Repositories::ChangelogService) diff --git a/spec/services/repositories/changelog_service_spec.rb b/spec/services/repositories/changelog_service_spec.rb index a545b0f070a..4ec692d678a 100644 --- a/spec/services/repositories/changelog_service_spec.rb +++ b/spec/services/repositories/changelog_service_spec.rb @@ -4,48 +4,64 @@ require 'spec_helper' RSpec.describe Repositories::ChangelogService do describe '#execute' do - it 'generates and commits a changelog section' do - project = create(:project, :empty_repo) - creator = project.creator - author1 = create(:user) - author2 = create(:user) - - project.add_maintainer(author1) - project.add_maintainer(author2) - - mr1 = create(:merge_request, :merged, target_project: project) - mr2 = create(:merge_request, :merged, target_project: project) - - # The range of commits ignores the first commit, but includes the last - # commit. To ensure both the commits below are included, we must create an - # extra commit. - # - # In the real world, the start commit of the range will be the last commit - # of the previous release, so ignoring that is expected and desired. - sha1 = create_commit( + let!(:project) { create(:project, :empty_repo) } + let!(:creator) { project.creator } + let!(:author1) { create(:user) } + let!(:author2) { create(:user) } + let!(:mr1) { create(:merge_request, :merged, target_project: project) } + let!(:mr2) { create(:merge_request, :merged, target_project: project) } + + # The range of commits ignores the first commit, but includes the last + # commit. To ensure both the commits below are included, we must create an + # extra commit. + # + # In the real world, the start commit of the range will be the last commit + # of the previous release, so ignoring that is expected and desired. + let!(:sha1) do + create_commit( project, creator, commit_message: 'Initial commit', actions: [{ action: 'create', content: 'test', file_path: 'README.md' }] ) + end + + let!(:sha2) do + project.add_maintainer(author1) - sha2 = create_commit( + create_commit( project, author1, commit_message: "Title 1\n\nChangelog: feature", actions: [{ action: 'create', content: 'foo', file_path: 'a.txt' }] ) + end + + let!(:sha3) do + project.add_maintainer(author2) - sha3 = create_commit( + create_commit( project, author2, commit_message: "Title 2\n\nChangelog: feature", actions: [{ action: 'create', content: 'bar', file_path: 'b.txt' }] ) + end + + let!(:sha4) do + create_commit( + project, + author2, + commit_message: "Title 3\n\nChangelog: feature", + actions: [{ action: 'create', content: 'bar', file_path: 'c.txt' }] + ) + end - commit1 = project.commit(sha2) - commit2 = project.commit(sha3) + let!(:commit1) { project.commit(sha2) } + let!(:commit2) { project.commit(sha3) } + let!(:commit3) { project.commit(sha4) } + it 'generates and commits a changelog section' do allow(MergeRequestDiffCommit) .to receive(:oldest_merge_request_id_per_commit) .with(project.id, [commit2.id, commit1.id]) @@ -54,17 +70,33 @@ RSpec.describe Repositories::ChangelogService do { sha: sha3, merge_request_id: mr2.id } ]) - recorder = ActiveRecord::QueryRecorder.new do - described_class - .new(project, creator, version: '1.0.0', from: sha1, to: sha3) - .execute - end + service = described_class + .new(project, creator, version: '1.0.0', from: sha1, to: sha3) + recorder = ActiveRecord::QueryRecorder.new { service.execute } changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data expect(recorder.count).to eq(10) expect(changelog).to include('Title 1', 'Title 2') end + + it 'uses the target branch when "to" is unspecified' do + allow(MergeRequestDiffCommit) + .to receive(:oldest_merge_request_id_per_commit) + .with(project.id, [commit3.id, commit2.id, commit1.id]) + .and_return([ + { sha: sha2, merge_request_id: mr1.id }, + { sha: sha3, merge_request_id: mr2.id } + ]) + + described_class + .new(project, creator, version: '1.0.0', from: sha1) + .execute + + changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data + + expect(changelog).to include('Title 1', 'Title 2', 'Title 3') + end end describe '#start_of_commit_range' do |