diff options
138 files changed, 2092 insertions, 427 deletions
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index abfbef4b2b0..b6702ae74b4 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,28 +4,28 @@ ### Target audience -<!--- For whom are we doing this? Include a [persona](https://design.gitlab.com/research/personas) +<!--- For whom are we doing this? Include a [persona](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/) listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A), or define a specific company role, e.g. "Release Manager". Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply) -- Parker, Product Manager, https://design.gitlab.com/research/personas#persona-parker +- Parker, Product Manager, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#parker-product-manager /label ~"Persona: Product Manager" -- Delaney, Development Team Lead, https://design.gitlab.com/research/personas#persona-delaney +- Delaney, Development Team Lead, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#delaney-development-team-lead /label ~"Persona: Development Team Lead" -- Sasha, Software Developer, https://design.gitlab.com/research/personas#persona-sasha +- Sasha, Software Developer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sasha-software-developer /label ~"Persona: Software developer" -- Devon, DevOps Engineer, https://design.gitlab.com/research/personas#persona-devon +- Devon, DevOps Engineer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#devon-devops-engineer /label ~"Persona: DevOps Engineer" -- Sidney, Systems Administrator, https://design.gitlab.com/research/personas#persona-sidney +- Sidney, Systems Administrator, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sidney-systems-administrator /label ~"Persona: Systems Administrator" -- Sam, Security Analyst, https://design.gitlab.com/research/personas#persona-sam +- Sam, Security Analyst, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sam-security-analyst /label ~"Persona: Security Analyst" --> diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index aaa16145399..9946651075f 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -30,6 +30,7 @@ Set the title to: `Description of the original issue` #### Documentation and final details - [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links) +- [ ] Add links to this issue and your MRs in the description of the security release issue - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 39893559155..3500250a4b0 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.20.0 +1.21.0 @@ -419,7 +419,8 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.12.0', require: 'gitaly' + gem 'grpc', '~> 1.15.0' gem 'google-protobuf', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index 841f85dc7e5..59e152c27fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.10.0) + gitaly-proto (1.12.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -1017,7 +1017,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.10.0) + gitaly-proto (~> 1.12.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index e2c304de00a..eef141a07ba 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -3,6 +3,7 @@ * Render environments table. */ import { GlLoadingIcon } from '@gitlab/ui'; +import _ from 'underscore'; import environmentItem from './environment_item.vue'; export default { @@ -24,6 +25,15 @@ export default { default: false, }, }, + computed: { + sortedEnvironments() { + return this.sortEnvironments(this.environments).map(env => + this.shouldRenderFolderContent(env) + ? { ...env, children: this.sortEnvironments(env.children) } + : env, + ); + }, + }, methods: { folderUrl(model) { return `${window.location.pathname}/folders/${model.folderName}`; @@ -31,6 +41,30 @@ export default { shouldRenderFolderContent(env) { return env.isFolder && env.isOpen && env.children && env.children.length > 0; }, + sortEnvironments(environments) { + /* + * The sorting algorithm should sort in the following priorities: + * + * 1. folders first, + * 2. last updated descending, + * 3. by name ascending, + * + * the sorting algorithm must: + * + * 1. Sort by name ascending, + * 2. Reverse (sort by name descending), + * 3. Sort by last deployment ascending, + * 4. Reverse (last deployment descending, name ascending), + * 5. Put folders first. + */ + return _.chain(environments) + .sortBy(env => (env.isFolder ? env.folderName : env.name)) + .reverse() + .sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000')) + .reverse() + .sortBy(env => (env.isFolder ? -1 : 1)) + .value(); + }, }, }; </script> @@ -53,7 +87,7 @@ export default { {{ s__('Environments|Updated') }} </div> </div> - <template v-for="(model, i) in environments" :model="model"> + <template v-for="(model, i) in sortedEnvironments" :model="model"> <div is="environment-item" :key="`environment-item-${i}`" diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 57ec6603d80..4d05f46ed17 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -96,6 +96,11 @@ export default class FilteredSearchDropdownManager { gl: DropdownNonUser, element: this.container.querySelector('#js-dropdown-wip'), }, + confidential: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-confidential'), + }, status: { reference: null, gl: NullDropdown, diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index b70da240833..48534bdf815 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -72,6 +72,23 @@ export default class FilteredSearchTokenKeys { ); } + addExtraTokensForIssues() { + const confidentialToken = { + key: 'confidential', + type: 'string', + param: '', + symbol: '', + icon: 'eye-slash', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: false, + capitalizeTokenValue: true, + }; + + this.tokenKeys.push(confidentialToken); + this.tokenKeysWithAlternative.push(confidentialToken); + } + addExtraTokensForMergeRequests() { const wipToken = { key: 'wip', diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index e03d6e9cd02..31164f74201 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -7,7 +7,9 @@ import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE, DISCUSSION_TAB_LABEL, + DISCUSSION_FILTER_TYPES, } from '../constants'; +import notesEventHub from '../event_hub'; export default { components: { @@ -46,6 +48,7 @@ export default { this.toggleFilters(currentTab); } + notesEventHub.$on('dropdownSelect', this.selectFilter); window.addEventListener('hashchange', this.handleLocationHash); this.handleLocationHash(); }, @@ -53,6 +56,7 @@ export default { this.toggleCommentsForm(); }, destroyed() { + notesEventHub.$off('dropdownSelect', this.selectFilter); window.removeEventListener('hashchange', this.handleLocationHash); }, methods: { @@ -86,12 +90,23 @@ export default { this.setTargetNoteHash(hash); } }, + filterType(value) { + if (value === 0) { + return DISCUSSION_FILTER_TYPES.ALL; + } else if (value === 1) { + return DISCUSSION_FILTER_TYPES.COMMENTS; + } + return DISCUSSION_FILTER_TYPES.HISTORY; + }, }, }; </script> <template> - <div v-if="displayFilters" class="discussion-filter-container d-inline-block align-bottom"> + <div + v-if="displayFilters" + class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom" + > <button id="discussion-filter-dropdown" ref="dropdownToggle" @@ -102,12 +117,17 @@ export default { {{ currentFilter.title }} <icon name="chevron-down" /> </button> <div + ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right" aria-labelledby="discussion-filter-dropdown" > <div class="dropdown-content"> <ul> - <li v-for="filter in filters" :key="filter.value"> + <li + v-for="filter in filters" + :key="filter.value" + :data-filter-type="filterType(filter.value)" + > <button :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue new file mode 100644 index 00000000000..46661e06f6d --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue @@ -0,0 +1,52 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { __, sprintf } from '~/locale'; + +import notesEventHub from '../event_hub'; + +export default { + components: { + GlButton, + Icon, + }, + computed: { + timelineContent() { + return sprintf( + __( + "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options.", + ), + { + startTag: `<b>`, + endTag: `</b>`, + }, + false, + ); + }, + }, + methods: { + selectFilter(value) { + notesEventHub.$emit('dropdownSelect', value); + }, + }, +}; +</script> + +<template> + <li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"> + <div class="timeline-icon"> + <icon name="comment" /> + </div> + <div class="timeline-content"> + <div v-html="timelineContent"></div> + <div class="discussion-filter-actions mt-2"> + <gl-button variant="default" @click="selectFilter(0)"> + {{ __('Show all activity') }} + </gl-button> + <gl-button variant="default" @click="selectFilter(1)"> + {{ __('Show comments only') }} + </gl-button> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 8d3f6d902f8..e2bd59f7631 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -6,6 +6,7 @@ import * as constants from '../constants'; import eventHub from '../event_hub'; import noteableNote from './noteable_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; +import discussionFilterNote from './discussion_filter_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; @@ -24,6 +25,7 @@ export default { placeholderNote, placeholderSystemNote, skeletonLoadingContainer, + discussionFilterNote, }, props: { noteableData: { @@ -235,6 +237,7 @@ export default { :help-page-path="helpPagePath" /> </template> + <discussion-filter-note v-show="commentsDisabled" /> </ul> <comment-form v-if="!commentsDisabled" :noteable-type="noteableType" /> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 78d365fe94b..fba3db8542c 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -24,3 +24,9 @@ export const NOTEABLE_TYPE_MAPPING = { MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE, Epic: EPIC_NOTEABLE_TYPE, }; + +export const DISCUSSION_FILTER_TYPES = { + ALL: 'all', + COMMENTS: 'comments', + HISTORY: 'history', +}; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 736c6a62610..35d4b034654 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,9 +1,11 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; -import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; document.addEventListener('DOMContentLoaded', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); + initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, isGroupDecendent: true, diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index a56c0bb6be8..8bf0c2edc71 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -4,11 +4,13 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; -import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); + initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, diff --git a/app/assets/javascripts/releases/store/actions.js b/app/assets/javascripts/releases/store/actions.js index baa2251403e..b5c4d54ac33 100644 --- a/app/assets/javascripts/releases/store/actions.js +++ b/app/assets/javascripts/releases/store/actions.js @@ -11,7 +11,7 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); /** * Fetches the main endpoint. * Will dispatch requestNamespace action before starting the request. - * Will dispatch receiveNamespaceSuccess if the request is successfull + * Will dispatch receiveNamespaceSuccess if the request is successful * Will dispatch receiveNamesapceError if the request returns an error * * @param {String} projectId diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index fa424532879..b09e44e052a 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -463,3 +463,7 @@ img.emoji { background-color: $gray-600; } } + +.cursor-pointer { + cursor: pointer; +} diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 1dfe2a69a2f..814e802f7c1 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -41,5 +41,6 @@ $spacers: ( 2: ($spacer), 3: ($spacer * 2), 4: ($spacer * 3), - 5: ($spacer * 4) + 5: ($spacer * 4), + 6: ($spacer * 8) ); diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7e7eff1346a..1198b9ea143 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -4,7 +4,7 @@ $note-form-margin-left: 72px; @mixin vertical-line($left) { &::before { - content: ''; + content: ""; border-left: 2px solid $gray-100; position: absolute; top: 0; @@ -53,12 +53,12 @@ $note-form-margin-left: 72px; &.note-form { margin-left: 0; - @include notes-media('min', map-get($grid-breakpoints, md)) { + @include notes-media("min", map-get($grid-breakpoints, md)) { margin-left: $note-form-margin-left; } .timeline-icon { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-left: -$note-icon-gutter-width; } } @@ -242,7 +242,7 @@ $note-form-margin-left: 72px; } .note-header { - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media("max", map-get($grid-breakpoints, xs)) { .inline { display: block; } @@ -303,28 +303,8 @@ $note-form-margin-left: 72px; } } - .timeline-icon { - float: left; - display: flex; - align-items: center; - background-color: $white-light; - width: $system-note-icon-size; - height: $system-note-icon-size; - border: 1px solid $border-color; - border-radius: $system-note-icon-size; - margin: -6px $gl-padding 0 0; - - svg { - width: $system-note-svg-size; - height: $system-note-svg-size; - fill: $gray-darkest; - display: block; - margin: 0 auto; - } - } - .timeline-content { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-left: 30px; } } @@ -368,7 +348,7 @@ $note-form-margin-left: 72px; } &::after { - content: ''; + content: ""; height: 70px; position: absolute; left: $gl-padding-24; @@ -380,6 +360,37 @@ $note-form-margin-left: 72px; } } } + + .system-note, + .discussion-filter-note { + .timeline-icon { + float: left; + display: flex; + align-items: center; + background-color: $white-light; + width: $system-note-icon-size; + height: $system-note-icon-size; + border: 1px solid $border-color; + border-radius: $system-note-icon-size; + margin: -6px $gl-padding 0 0; + + svg { + width: $system-note-svg-size; + height: $system-note-svg-size; + fill: $gray-darkest; + display: block; + margin: 0 auto; + } + } + } + + .discussion-filter-note { + .timeline-icon { + width: $system-note-icon-size + 6; + height: $system-note-icon-size + 6; + margin-top: -8px; + } + } } // Diff code in discussion view @@ -579,7 +590,7 @@ $note-form-margin-left: 72px; .note-headline-light { display: inline; - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media("max", map-get($grid-breakpoints, xs)) { display: block; } } @@ -645,7 +656,7 @@ $note-form-margin-left: 72px; margin-left: 10px; color: $gray-darkest; - @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { + @include notes-media("max", map-get($grid-breakpoints, sm) - 1) { float: none; margin-left: 0; } @@ -764,7 +775,7 @@ $note-form-margin-left: 72px; } .line-resolve-all-container { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-right: 0; } @@ -905,7 +916,6 @@ $note-form-margin-left: 72px; } .discussion-filter-container { - .btn > svg { width: $gl-col-padding; height: $gl-col-padding; @@ -927,7 +937,6 @@ $note-form-margin-left: 72px; //This needs to be deleted when Snippet/Commit comments are convered to Vue // See https://gitlab.com/gitlab-org/gitlab-ce/issues/53918#note_117038785 .unstyled-comments { - .discussion-header { padding: $gl-padding; border-bottom: 1px solid $border-color; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index af0b0c64814..b7eb6af6d67 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -43,7 +43,10 @@ class ApplicationController < ActionController::Base :git_import_enabled?, :gitlab_project_import_enabled?, :manifest_import_enabled? + # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security + # concerns due to caching private data. DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze + DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache".freeze rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -235,9 +238,9 @@ class ApplicationController < ActionController::Base end def no_cache_headers - response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE + headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility + headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' end def default_headers @@ -247,10 +250,16 @@ class ApplicationController < ActionController::Base headers['X-Content-Type-Options'] = 'nosniff' if current_user - # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security - # concerns due to caching private data. - headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL - headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility + headers['Cache-Control'] = default_cache_control + headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility + end + end + + def default_cache_control + if request.xhr? + ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL + else + DEFAULT_GITLAB_CACHE_CONTROL end end diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 3bd91b71d92..68a2a83f0de 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -24,7 +24,7 @@ class Clusters::ClustersController < Clusters::BaseController # Note: We are paginating through an array here but this should OK as: # # In CE, we can have a maximum group nesting depth of 21, so including - # project cluster, we can have max 22 clusters for a group hierachy. + # project cluster, we can have max 22 clusters for a group hierarchy. # In EE (Premium) we can have any number, as multiple clusters are # supported, but the number of clusters are fairly low currently. # diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 07d0bf16d93..c529aabf797 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -91,6 +91,7 @@ module IssuableCollections options = { scope: params[:scope], state: params[:state], + confidential: Gitlab::Utils.to_boolean(params[:confidential]), sort: set_sort_order } diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 5572c3cee2d..57e444319e0 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -123,7 +123,7 @@ module LfsRequest (authentication_abilities || []).include?(capability) end - # Overriden in EE + # Overridden in EE def limit_exceeded? false end diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb index fbb1cfc5c66..8d936b8121c 100644 --- a/app/finders/admin/runners_finder.rb +++ b/app/finders/admin/runners_finder.rb @@ -14,7 +14,7 @@ class Admin::RunnersFinder < UnionFinder sort! paginate! - @runners + @runners.with_tags end def sort_key diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 96a36db7ec8..ec340f38450 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -134,7 +134,7 @@ class GroupDescendantsFinder def subgroups return Group.none unless Group.supports_nested_objects? - # When filtering subgroups, we want to find all matches withing the tree of + # When filtering subgroups, we want to find all matches within the tree of # descendants to show to the user groups = if params[:filter] subgroups_matching_filter diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index a0504ca0879..cb44575d6f1 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -69,7 +69,16 @@ class IssuesFinder < IssuableFinder end def filter_items(items) - by_due_date(super) + issues = super + issues = by_due_date(issues) + issues = by_confidential(issues) + issues + end + + def by_confidential(items) + return items if params[:confidential].nil? + + params[:confidential] ? items.confidential_only : items.public_only end def by_due_date(items) diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb index 13839474e1f..62bb2e4da23 100644 --- a/app/helpers/count_helper.rb +++ b/app/helpers/count_helper.rb @@ -13,7 +13,7 @@ module CountHelper # memberships, and deducting 1 for each root of the fork network. # This might be inacurate as the root of the fork network might have been deleted. # - # This makes querying this information a lot more effecient and it should be + # This makes querying this information a lot more efficient and it should be # accurate enough for the instance wide statistics def approximate_fork_count_with_delimiters(count_data) fork_network_count = count_data[ForkNetwork] diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index da08214963f..33e61cd2111 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -18,7 +18,7 @@ module Ci FailedToPersistDataError = Class.new(StandardError) # Note: The ordering of this enum is related to the precedence of persist store. - # The bottom item takes the higest precedence, and the top item takes the lowest precedence. + # The bottom item takes the highest precedence, and the top item takes the lowest precedence. enum data_store: { redis: 1, database: 2, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index eb15347b4e1..c0ebafd613d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -652,9 +652,9 @@ module Ci def all_merge_requests @all_merge_requests ||= if merge_request? - project.merge_requests.where(id: merge_request_id) + MergeRequest.where(id: merge_request_id) else - project.merge_requests.where(source_branch: ref) + MergeRequest.where(source_project_id: project_id, source_branch: ref) end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 5aae31de6e2..d82e11bbb89 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -97,6 +97,7 @@ module Ci scope :order_contacted_at_asc, -> { order(contacted_at: :asc) } scope :order_created_at_desc, -> { order(created_at: :desc) } + scope :with_tags, -> { preload(:tags) } validate :tag_constraints validates :access_level, presence: true diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb index 1e3afd641ed..f862031bce0 100644 --- a/app/models/concerns/fast_destroy_all.rb +++ b/app/models/concerns/fast_destroy_all.rb @@ -11,7 +11,7 @@ # it is difficult to accomplish it. # # This module defines a format to use `delete_all` and delete associated external data. -# Here is an exmaple +# Here is an example # # Situation # - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build` diff --git a/app/models/concerns/iid_routes.rb b/app/models/concerns/iid_routes.rb index b7f99e845ca..3eeb29b6595 100644 --- a/app/models/concerns/iid_routes.rb +++ b/app/models/concerns/iid_routes.rb @@ -4,7 +4,7 @@ module IidRoutes ## # This automagically enforces all related routes to use `iid` instead of `id` # If you want to use `iid` for some routes and `id` for other routes, this module should not to be included, - # instead you should define `iid` or `id` explictly at each route generators. e.g. pipeline_path(project.id, pipeline.iid) + # instead you should define `iid` or `id` explicitly at each route generators. e.g. pipeline_path(project.id, pipeline.iid) def to_param iid.to_s end diff --git a/app/models/environment.rb b/app/models/environment.rb index 1fc088b12ae..87bdb52b58b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -243,6 +243,10 @@ class Environment < ActiveRecord::Base self.environment_type || self.name end + def name_without_type + @name_without_type ||= name.delete_prefix("#{environment_type}/") + end + def deployment_platform strong_memoize(:deployment_platform) do project.deployment_platform(environment: self.name) diff --git a/app/models/issue.rb b/app/models/issue.rb index 182c5d3d4b0..0b46e949052 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -66,6 +66,7 @@ class Issue < ActiveRecord::Base scope :preload_associations, -> { preload(:labels, project: :namespace) } scope :public_only, -> { where(confidential: false) } + scope :confidential_only, -> { where(confidential: true) } after_save :expire_etag_cache after_save :ensure_metrics, unless: :imported? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 75fca96ce0a..1468ae1c34a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -764,6 +764,16 @@ class MergeRequest < ActiveRecord::Base true end + def mergeable_to_ref? + return false if merged? + return false if broken? + + # Given the `merge_ref_path` will have the same + # state the `target_branch` would have. Ideally + # we need to check if it can be merged to it. + project.repository.can_be_merged?(diff_head_sha, target_branch) + end + def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) end @@ -1077,6 +1087,10 @@ class MergeRequest < ActiveRecord::Base "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" end + def merge_ref_path + "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge" + end + def in_locked_state begin lock_mr diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 9f16eefe074..481c1d963c6 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -153,7 +153,7 @@ class NotificationRecipient user.global_notification_setting end - # Returns the notificaton_setting of the lowest group in hierarchy with non global level + # Returns the notification_setting of the lowest group in hierarchy with non global level def closest_non_global_group_notification_settting return unless @group return if indexed_group_notification_settings.empty? diff --git a/app/models/project.rb b/app/models/project.rb index 58254eb1bc9..b016a65f0bb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -160,6 +160,7 @@ class Project < ActiveRecord::Base has_one :pushover_service has_one :jira_service has_one :redmine_service + has_one :youtrack_service has_one :custom_issue_tracker_service has_one :bugzilla_service has_one :gitlab_issue_tracker_service, inverse_of: :project @@ -248,10 +249,10 @@ class Project < ActiveRecord::Base has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :commit_statuses - # The relation :all_pipelines is intented to be used when we want to get the + # The relation :all_pipelines is intended to be used when we want to get the # whole list of pipelines associated to the project has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project - # The relation :ci_pipelines is intented to be used when we want to get only + # The relation :ci_pipelines is intended to be used when we want to get only # those pipeline which are directly related to CI. There are # other pipelines, like webide ones, that we won't retrieve # if we use this relation. @@ -1215,7 +1216,7 @@ class Project < ActiveRecord::Base "#{web_url}.git" end - # Is overriden in EE + # Is overridden in EE def lfs_http_url_to_repo(_) http_url_to_repo end @@ -1838,7 +1839,7 @@ class Project < ActiveRecord::Base # Set repository as writable again def set_repository_writable! with_lock do - update_column(repository_read_only, false) + update_column(:repository_read_only, false) end end @@ -1925,6 +1926,14 @@ class Project < ActiveRecord::Base persisted? && path_changed? end + def human_merge_method + if merge_method == :ff + 'Fast-forward' + else + merge_method.to_s.humanize + end + end + def merge_method if self.merge_requests_ff_only_enabled :ff diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb new file mode 100644 index 00000000000..957be685aea --- /dev/null +++ b/app/models/project_services/youtrack_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class YoutrackService < IssueTrackerService + validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? + + prop_accessor :description, :project_url, :issues_url + + # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 + def self.reference_pattern(only_long: false) + if only_long + /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)/ + else + /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/ + end + end + + def title + 'YouTrack' + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'YouTrack issue tracker' + end + end + + def self.to_param + 'youtrack' + end + + def fields + [ + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url', required: true }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true } + ] + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index ed55a6e572b..cd761a29618 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -854,6 +854,12 @@ class Repository end end + def merge_to_ref(user, source_sha, merge_request, target_ref, message) + branch = merge_request.target_branch + + raw.merge_to_ref(user, source_sha, branch, target_ref, message) + end + def ff_merge(user, source, target_branch, merge_request: nil) their_commit_id = commit(source)&.id raise 'Invalid merge source' if their_commit_id.nil? diff --git a/app/models/service.rb b/app/models/service.rb index 3461e0bfe70..da523bfa426 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -266,6 +266,7 @@ class Service < ActiveRecord::Base prometheus pushover redmine + youtrack slack_slash_commands slack teamcity diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 4a7d13915dd..76248e6470e 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -8,6 +8,7 @@ class EnvironmentEntity < Grape::Entity expose :state expose :external_url expose :environment_type + expose :name_without_type expose :last_deployment, using: DeploymentEntity expose :stop_action_available?, as: :has_stop_action diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index c4f69175de3..35a0efcd0a1 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -112,10 +112,10 @@ module Ci def extra_options(options = {}) # In Ruby 2.4, even when options is empty, f(**options) doesn't work when f # doesn't have any parameters. We reproduce the Ruby 2.5 behavior by - # checking explicitely that no arguments are given. + # checking explicitly that no arguments are given. raise ArgumentError if options.any? - {} # overriden in EE + {} # overridden in EE end end end diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 4ba3f5fb8ba..2dbb7c3917d 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -38,11 +38,11 @@ module Ci end def create_pipeline_from_job(job) - # overriden in EE + # overridden in EE end def job_from_token - # overriden in EE + # overridden in EE end def variables diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index 92c2c1b9834..12f8c849d41 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -27,9 +27,11 @@ module Clusters application.oauth_application = create_oauth_application(application, request) end - application.save! + worker = application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker - Clusters::Applications::ScheduleInstallationService.new(application).execute + application.make_scheduled! + + worker.perform_async(application.name, application.id) end end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb deleted file mode 100644 index 15c93f1e79b..00000000000 --- a/app/services/clusters/applications/schedule_installation_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Applications - class ScheduleInstallationService - attr_reader :application - - def initialize(application) - @application = application - end - - def execute - application.updateable? ? schedule_upgrade : schedule_install - end - - private - - def schedule_upgrade - application.make_scheduled! - - ClusterUpgradeAppWorker.perform_async(application.name, application.id) - end - - def schedule_install - application.make_scheduled! - - ClusterInstallAppWorker.perform_async(application.name, application.id) - end - end - end -end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 092fd64574d..f387c749a21 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -235,6 +235,6 @@ class GitPushService < BaseService private def pipeline_options - {} # to be overriden in EE + {} # to be overridden in EE end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 6fef5b3ed1d..e39b3603c6c 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -61,6 +61,6 @@ class GitTagPushService < BaseService end def pipeline_options - {} # to be overriden in EE + {} # to be overridden in EE end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 55a3b9fa7b1..99ead467f74 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -33,7 +33,7 @@ module Groups private def after_build_hook(group, params) - # overriden in EE + # overridden in EE end def create_chat_team? diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 9ff1da270e2..787445180f0 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -31,7 +31,7 @@ module Groups private def before_assignment_hook(group, params) - # overriden in EE + # overridden in EE end def after_update diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb new file mode 100644 index 00000000000..095bdca5472 --- /dev/null +++ b/app/services/merge_requests/merge_base_service.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module MergeRequests + class MergeBaseService < MergeRequests::BaseService + include Gitlab::Utils::StrongMemoize + + MergeError = Class.new(StandardError) + + attr_reader :merge_request + + # Overridden in EE. + def hooks_validation_pass?(_merge_request) + true + end + + # Overridden in EE. + def hooks_validation_error(_merge_request) + # No-op + end + + def source + if merge_request.squash + squash_sha! + else + merge_request.diff_head_sha + end + end + + private + + # Overridden in EE. + def error_check! + # No-op + end + + def raise_error(message) + raise MergeError, message + end + + def handle_merge_error(*args) + # No-op + end + + def commit_message + params[:commit_message] || + merge_request.default_merge_commit_message + end + + def squash_sha! + strong_memoize(:squash_sha) do + params[:merge_request] = merge_request + squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute + + case squash_result[:status] + when :success + squash_result[:squash_sha] + when :error + raise ::MergeRequests::MergeService::MergeError, squash_result[:message] + end + end + end + end +end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 449997bcf07..8241e408ce5 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -7,13 +7,7 @@ module MergeRequests # mark merge request as merged and execute all hooks and notifications # Executed when you do merge via GitLab UI # - class MergeService < MergeRequests::BaseService - include Gitlab::Utils::StrongMemoize - - MergeError = Class.new(StandardError) - - attr_reader :merge_request, :source - + class MergeService < MergeRequests::MergeBaseService delegate :merge_jid, :state, to: :@merge_request def execute(merge_request) @@ -24,7 +18,7 @@ module MergeRequests @merge_request = merge_request - error_check! + validate! merge_request.in_locked_state do if commit @@ -38,22 +32,22 @@ module MergeRequests handle_merge_error(log_message: e.message, save_message_on_model: true) end - def source - if merge_request.squash - squash_sha! - else - merge_request.diff_head_sha - end - end + private - # Overridden in EE. - def hooks_validation_pass?(_merge_request) - true + def validate! + authorization_check! + error_check! end - private + def authorization_check! + unless @merge_request.can_be_merged_by?(current_user) + raise_error('You are not allowed to merge this merge request') + end + end def error_check! + super + error = if @merge_request.should_be_rebased? 'Only fast-forward merge is allowed for your project. Please update your source branch' @@ -63,7 +57,7 @@ module MergeRequests 'No source for merge' end - raise MergeError, error if error + raise_error(error) if error end def commit @@ -73,36 +67,20 @@ module MergeRequests if commit_id log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}") else - raise MergeError, 'Conflicts detected during merge' + raise_error('Conflicts detected during merge') end merge_request.update!(merge_commit_sha: commit_id) end - def squash_sha! - strong_memoize(:squash_sha) do - params[:merge_request] = merge_request - squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute - - case squash_result[:status] - when :success - squash_result[:squash_sha] - when :error - raise ::MergeRequests::MergeService::MergeError, squash_result[:message] - end - end - end - def try_merge - message = params[:commit_message] || merge_request.default_merge_commit_message - - repository.merge(current_user, source, merge_request, message) + repository.merge(current_user, source, merge_request, commit_message) rescue Gitlab::Git::PreReceiveError => e handle_merge_error(log_message: e.message) - raise MergeError, 'Something went wrong during merge pre-receive hook' + raise_error('Something went wrong during merge pre-receive hook') rescue => e handle_merge_error(log_message: e.message) - raise MergeError, 'Something went wrong during merge' + raise_error('Something went wrong during merge') ensure merge_request.update!(in_progress_merge_commit_sha: nil) end diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb new file mode 100644 index 00000000000..586652ae44e --- /dev/null +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module MergeRequests + # Performs the merge between source SHA and the target branch. Instead + # of writing the result to the MR target branch, it targets the `target_ref`. + # + # Ideally this should leave the `target_ref` state with the same state the + # target branch would have if we used the regular `MergeService`, but without + # every side-effect that comes with it (MR updates, mails, source branch + # deletion, etc). This service should be kept idempotent (i.e. can + # be executed regardless of the `target_ref` current state). + # + class MergeToRefService < MergeRequests::MergeBaseService + def execute(merge_request) + @merge_request = merge_request + + validate! + + commit_id = commit + + raise_error('Conflicts detected during merge') unless commit_id + + success(commit_id: commit_id) + rescue MergeError => error + error(error.message) + end + + private + + def validate! + authorization_check! + error_check! + end + + def error_check! + super + + error = + if Feature.disabled?(:merge_to_tmp_merge_ref_path, project) + 'Feature is not enabled' + elsif !merge_method_supported? + "#{project.human_merge_method} to #{target_ref} is currently not supported." + elsif !hooks_validation_pass?(merge_request) + hooks_validation_error(merge_request) + elsif @merge_request.should_be_rebased? + 'Fast-forward merge is not possible. Please update your source branch.' + elsif !@merge_request.mergeable_to_ref? + "Merge request is not mergeable to #{target_ref}" + elsif !source + 'No source for merge' + end + + raise_error(error) if error + end + + def authorization_check! + unless Ability.allowed?(current_user, :admin_merge_request, project) + raise_error("You are not allowed to merge to this ref") + end + end + + def target_ref + merge_request.merge_ref_path + end + + def commit + repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message) + rescue Gitlab::Git::PreReceiveError => error + raise MergeError, error.message + end + + def merge_method_supported? + [:merge, :rebase_merge].include?(project.merge_method) + end + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ec6c306227b..ea8ac7e4656 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -360,7 +360,7 @@ module SystemNoteService # author - User performing the change # branch_type - 'source' or 'target' # old_branch - old branch name - # new_branch - new branch nmae + # new_branch - new branch name # # Example Note text: # diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 4641986cb56..ecf2b1d60ba 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -49,7 +49,7 @@ .table-section.section-10.section-wrap .table-mobile-header{ role: 'rowheader' }= _('Tags') .table-mobile-content - - runner.tag_list.sort.each do |tag| + - runner.tags.map(&:name).sort.each do |tag| %span.badge.badge-primary = tag diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml index ce3ef29c32e..74478ee011c 100644 --- a/app/views/projects/pages/_https_only.html.haml +++ b/app/views/projects/pages/_https_only.html.haml @@ -3,7 +3,7 @@ .form-check = f.check_box :pages_https_only, class: 'form-check-input', disabled: pages_https_only_disabled? = f.label :pages_https_only, class: pages_https_only_label_class do - %strong Force domains with SSL certificates to use HTTPS + %strong Force HTTPS (requires valid certificates) - unless pages_https_only_disabled? .prepend-top-10 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 588659c7e9c..bdba47ed14d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -128,6 +128,14 @@ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } %button.btn.btn-link{ type: 'button' } = _('No') + #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('No') = render_if_exists 'shared/issuable/filter_weight', type: type diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index 148384600b6..2c070d482a1 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -37,9 +37,9 @@ class ExpirePipelineCacheWorker Gitlab::Routing.url_helpers.project_new_merge_request_path(project, format: :json) end - def each_pipelines_merge_request_path(project, pipeline) + def each_pipelines_merge_request_path(pipeline) pipeline.all_merge_requests.each do |merge_request| - path = Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(project, merge_request, format: :json) + path = Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json) yield(path) end @@ -59,7 +59,7 @@ class ExpirePipelineCacheWorker store.touch(project_pipeline_path(project, pipeline)) store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil? store.touch(new_merge_request_pipelines_path(project)) - each_pipelines_merge_request_path(project, pipeline) do |path| + each_pipelines_merge_request_path(pipeline) do |path| store.touch(path) end end diff --git a/changelogs/unreleased/46750-ci-empty-environment-is-created-even-when-a-job-isn-t-run-when-manual.yml b/changelogs/unreleased/46750-ci-empty-environment-is-created-even-when-a-job-isn-t-run-when-manual.yml new file mode 100644 index 00000000000..d052a28ab51 --- /dev/null +++ b/changelogs/unreleased/46750-ci-empty-environment-is-created-even-when-a-job-isn-t-run-when-manual.yml @@ -0,0 +1,5 @@ +--- +title: Sort Environments by Last Updated +merge_request: 25260 +author: +type: added diff --git a/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml b/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml new file mode 100644 index 00000000000..76ea4149c56 --- /dev/null +++ b/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml @@ -0,0 +1,5 @@ +--- +title: Add support for toggling discussion filter from notes section +merge_request: 25426 +author: +type: added diff --git a/changelogs/unreleased/53861-api-promote-project-milestone-to-a-group-milestone.yml b/changelogs/unreleased/53861-api-promote-project-milestone-to-a-group-milestone.yml new file mode 100644 index 00000000000..6c621763e2e --- /dev/null +++ b/changelogs/unreleased/53861-api-promote-project-milestone-to-a-group-milestone.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Promote project milestone to a group milestone' +merge_request: 25203 +author: Nermin Vehabovic +type: added diff --git a/changelogs/unreleased/57905-etag-caching-probably-broken-since-11-5-0.yml b/changelogs/unreleased/57905-etag-caching-probably-broken-since-11-5-0.yml new file mode 100644 index 00000000000..046ef8ee99e --- /dev/null +++ b/changelogs/unreleased/57905-etag-caching-probably-broken-since-11-5-0.yml @@ -0,0 +1,5 @@ +--- +title: Fix ETag caching not being used for AJAX requests +merge_request: 25400 +author: +type: fixed diff --git a/changelogs/unreleased/add-youtrack-integration.yml b/changelogs/unreleased/add-youtrack-integration.yml new file mode 100644 index 00000000000..f500e625145 --- /dev/null +++ b/changelogs/unreleased/add-youtrack-integration.yml @@ -0,0 +1,5 @@ +--- +title: Add YouTrack integration service +merge_request: 25361 +author: Yauhen Kotau @bessorion +type: added diff --git a/changelogs/unreleased/filter-confidential-issues.yml b/changelogs/unreleased/filter-confidential-issues.yml new file mode 100644 index 00000000000..83f19a57aab --- /dev/null +++ b/changelogs/unreleased/filter-confidential-issues.yml @@ -0,0 +1,5 @@ +--- +title: Ability to filter confidential issues +merge_request: 24960 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/jc-fix-set-project-writable.yml b/changelogs/unreleased/jc-fix-set-project-writable.yml new file mode 100644 index 00000000000..0bfd90c3967 --- /dev/null +++ b/changelogs/unreleased/jc-fix-set-project-writable.yml @@ -0,0 +1,5 @@ +--- +title: Fix method to mark a project repository as writable +merge_request: 25546 +author: +type: fixed diff --git a/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml b/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml new file mode 100644 index 00000000000..012b547a630 --- /dev/null +++ b/changelogs/unreleased/osw-create-and-store-merge-ref-for-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Support merge ref writing (without merging to target branch) +merge_request: 24692 +author: +type: added diff --git a/changelogs/unreleased/patch-45.yml b/changelogs/unreleased/patch-45.yml new file mode 100644 index 00000000000..94fa1d29b32 --- /dev/null +++ b/changelogs/unreleased/patch-45.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect Pages Domains checkbox description. +merge_request: 25392 +author: Anton Melser +type: other diff --git a/changelogs/unreleased/sh-fix-cpp-templates-404.yml b/changelogs/unreleased/sh-fix-cpp-templates-404.yml new file mode 100644 index 00000000000..ac958d84099 --- /dev/null +++ b/changelogs/unreleased/sh-fix-cpp-templates-404.yml @@ -0,0 +1,5 @@ +--- +title: Fix 404s when C++ .gitignore template selected +merge_request: 25416 +author: +type: fixed diff --git a/changelogs/unreleased/sh-remove-nplusone-admin-runners-tags.yml b/changelogs/unreleased/sh-remove-nplusone-admin-runners-tags.yml new file mode 100644 index 00000000000..f8ac345bc95 --- /dev/null +++ b/changelogs/unreleased/sh-remove-nplusone-admin-runners-tags.yml @@ -0,0 +1,5 @@ +--- +title: Remove N+1 query for tags in /admin/runners page +merge_request: 25572 +author: +type: performance diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index be4183f39be..69a60a65e77 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -11,6 +11,10 @@ queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE # Default is to retry 25 times with exponential backoff. That's too much. Sidekiq.default_worker_options = { retry: 3 } +if Rails.env.development? + Sidekiq.default_worker_options[:backtrace] = true +end + enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' Sidekiq.configure_server do |config| diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index e554c06532e..d95c3acec54 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -146,6 +146,14 @@ the share is exported and exists on the NFS server and try to remount. --- +## Upgrading GitLab HA + +GitLab HA installations can be upgraded with no downtime, but the +upgrade process must be carefully coordinated to avoid failures. See the +[Omnibus GitLab multi-node upgrade +document](https://docs.gitlab.com/omnibus/update/#multi-node--ha-deployment) +for more details. + Read more on high-availability configuration: 1. [Configure the database](database.md) diff --git a/doc/api/issues.md b/doc/api/issues.md index 0571f280d2a..cb5789e76b7 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -32,6 +32,7 @@ GET /issues?author_id=5 GET /issues?assignee_id=5 GET /issues?my_reaction_emoji=star GET /issues?search=foo&in=title +GET /issues?confidential=true ``` | Attribute | Type | Required | Description | @@ -52,6 +53,7 @@ GET /issues?search=foo&in=title | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues @@ -148,6 +150,7 @@ GET /groups/:id/issues?search=issue+title+or+description GET /groups/:id/issues?author_id=5 GET /groups/:id/issues?assignee_id=5 GET /groups/:id/issues?my_reaction_emoji=star +GET /groups/:id/issues?confidential=true ``` | Attribute | Type | Required | Description | @@ -168,6 +171,7 @@ GET /groups/:id/issues?my_reaction_emoji=star | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues @@ -264,6 +268,7 @@ GET /projects/:id/issues?search=issue+title+or+description GET /projects/:id/issues?author_id=5 GET /projects/:id/issues?assignee_id=5 GET /projects/:id/issues?my_reaction_emoji=star +GET /projects/:id/issues?confidential=true ``` | Attribute | Type | Required | Description | @@ -284,6 +289,8 @@ GET /projects/:id/issues?my_reaction_emoji=star | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues diff --git a/doc/api/milestones.md b/doc/api/milestones.md index fa8f8a0bcf0..897184d51af 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -130,3 +130,18 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `milestone_id` (required) - The ID of a project milestone + +## Promote project milestone to a group milestone + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53861) in GitLab 11.9 + +Only for users with developer access to the group. + +``` +POST /projects/:id/milestones/:milestone_id/promote +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of a project milestone diff --git a/doc/api/services.md b/doc/api/services.md index 9275a1ccda8..c44f5cc5781 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -1102,3 +1102,39 @@ GET /projects/:id/services/mock-ci ``` [11435]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11435 + +## YouTrack + +YouTrack issue tracker + +### Create/Edit YouTrack service + +Set YouTrack service for a project. + +``` +PUT /projects/:id/services/youtrack +``` + +Parameters: + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issues_url` | string | true | Issue url | +| `project_url` | string | true | Project url | +| `description` | string | false | Description | + +### Delete YouTrack Service + +Delete YouTrack service for a project. + +``` +DELETE /projects/:id/services/youtrack +``` + +### Get YouTrack Service Settings + +Get YouTrack service settings for a project. + +``` +GET /projects/:id/services/youtrack +``` diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 6054873cb46..dd338f6f67d 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -47,6 +47,7 @@ are very appreciative of the work done by translators and proofreaders! - Hungarian - Proofreaders needed. - Indonesian + - Adi Ferdian - [GitLab](https://gitlab.com/adiferd), [Crowdin](https://crowdin.com/profile/adiferd) - Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm) - Italian - Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo) diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 075feaeead9..edd1af423ca 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,8 +1,8 @@ # External issue tracker GitLab has a great issue tracker but you can also use an external one such as -Jira, Redmine, or Bugzilla. Issue trackers are configurable per GitLab project and allow -you to do the following: +Jira, Redmine, YouTrack, or Bugzilla. Issue trackers are configurable per GitLab project +and allow you to do the following: - you can reference these external issues inside GitLab interface (merge requests, commits, comments) and they will be automatically converted @@ -20,6 +20,7 @@ To enable an external issue tracker you must configure the appropriate **Service Visit the links below for details: - [Redmine](../user/project/integrations/redmine.md) +- [YouTrack](../user/project/integrations/youtrack.md) - [Jira](../user/project/integrations/jira.md) - [Bugzilla](../user/project/integrations/bugzilla.md) - [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 7a18354bf66..2e1df9d50d4 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -697,7 +697,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | | `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | | `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-environment-variables). Set it to use a custom database name. | -| `POSTGRES_VERSION` | The PostgreSQL version; defaults to `9.6.2` | +| `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.2`. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | | `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| | `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index cec9018b67f..e2f23827360 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -50,6 +50,7 @@ Click on the service links to see further configuration instructions and details | [Prometheus](prometheus.md) | Monitor the performance of your deployed apps | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | +| [YouTrack](youtrack.md) | YouTrack issue tracker | ## Services templates diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md new file mode 100644 index 00000000000..2ab14a8db2c --- /dev/null +++ b/doc/user/project/integrations/youtrack.md @@ -0,0 +1,31 @@ +# YouTrack Service + +JetBrains YouTrack is a web-based issue tracking and project management platform. +Please refer official [documentation](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html) for details about YouTrack itself. + + +1. To enable the YouTrack integration in a project, navigate to the +[Integrations page](project_services.md#accessing-the-project-services), click +the **YouTrack** service, and fill in the required details on the page as described +in the table below. + + | Field | Description | + | ----- | ----------- | + | `description` | A name for the issue tracker (to differentiate between instances, for example) | + | `project_url` | The URL to the project in YouTrack which is being linked to this GitLab project | + | `issues_url` | The URL to the issue in YouTrack project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | + + Once you have configured and enabled YouTrack you'll see the YouTrack link on the GitLab project pages that takes you to the appropriate YouTrack project. + +1. To disable the internal issue tracking system in a project, navigate to the General page, expand [Permissions](../settings/index.md#sharing-and-permissions), and slide the Issues switch invalid. + + ![Issue configuration](img/issue_configuration.png) + +## Referencing issues in YouTrack + +Issues in YouTrack can be referenced as `<PROJECT>-<ID>` where `<PROJECT>` +starts with a capital letter which is then followed by capital or lower case +letters, numbers or underscores, and `<ID>` is a number (example `Api_32-143`). + +`<PROJECT>` part is included into issue_id and links can point any YouTrack +project (`issues_url` + issue_id) diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 5a3ac9c175b..907a305fe23 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -155,7 +155,7 @@ For further details, see [Importing issues from CSV](csv_import.md) Alternatively to GitLab's built-in Issue Tracker, you can also use an [external tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, -or Bugzilla. +YouTrack, or Bugzilla. ### Issue API diff --git a/jest.config.js b/jest.config.js index 3fa39dd7e8d..5ee56b244c7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,6 +20,7 @@ module.exports = { '^ee(.*)$': '<rootDir>/ee/app/assets/javascripts$1', '^helpers(.*)$': '<rootDir>/spec/frontend/helpers$1', '^vendor(.*)$': '<rootDir>/vendor/assets/javascripts$1', + '\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js', }, collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'], coverageDirectory: '<rootDir>/coverage-frontend/', @@ -30,6 +31,7 @@ module.exports = { setupTestFrameworkScriptFile: '<rootDir>/spec/frontend/test_setup.js', restoreMocks: true, transform: { + '^.+\\.(gql|graphql)$': 'jest-transform-graphql', '^.+\\.js$': 'babel-jest', '^.+\\.vue$': 'vue-jest', }, diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 94ed9ac6fb1..f43f4d961d6 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -54,6 +54,7 @@ module API optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' + optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' use :pagination use :issues_params_ee diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index da31bcb8dac..ca24742b7a3 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -98,6 +98,23 @@ module API milestone_issuables_for(user_project, :merge_request) end + + desc 'Promote a milestone to group milestone' do + detail 'This feature was introduced in GitLab 11.9' + end + post ':id/milestones/:milestone_id/promote' do + begin + authorize! :admin_milestone, user_project + authorize! :admin_milestone, user_project.group + + milestone = user_project.milestones.find(params[:milestone_id]) + Milestones::PromoteService.new(user_project, current_user).execute(milestone) + + status(200) + rescue Milestones::PromoteService::PromoteMilestoneError => error + render_api_error!(error.message, 400) + end + end end end end diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index d05ddad7466..119902a189c 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -36,7 +36,10 @@ module API optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses' optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses' end - get ':id/templates/:type/:name', requirements: { name: /[\w\.-]+/ } do + # The regex is needed to ensure a period (e.g. agpl-3.0) + # isn't confused with a format type. We also need to allow encoded + # values (e.g. C%2B%2B for C++), so allow % and + as well. + get ':id/templates/:type/:name', requirements: { name: /[\w%.+-]+/ } do template = TemplateFinder .build(params[:type], user_project, name: params[:name]) .execute diff --git a/lib/api/services.rb b/lib/api/services.rb index 145897516a0..bda6be51553 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -592,6 +592,26 @@ module API desc: 'The description of the tracker' } ], + 'youtrack' => [ + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], 'slack' => [ CHAT_NOTIFICATION_SETTINGS, CHAT_NOTIFICATION_FLAGS, @@ -665,6 +685,7 @@ module API PrometheusService, PushoverService, RedmineService, + YoutrackService, SlackService, MattermostService, MicrosoftTeamsService, diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 593a3676519..aea132a3dd9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -556,6 +556,12 @@ module Gitlab tags.find { |tag| tag.name == name } end + def merge_to_ref(user, source_sha, branch, target_ref, message) + wrapped_gitaly_errors do + gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message) + end + end + def merge(user, source_sha, target_branch, message, &block) wrapped_gitaly_errors do gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 22d2d149e65..d172c798da2 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -100,6 +100,25 @@ module Gitlab end end + def user_merge_to_ref(user, source_sha, branch, target_ref, message) + request = Gitaly::UserMergeToRefRequest.new( + repository: @gitaly_repo, + source_sha: source_sha, + branch: encode_binary(branch), + target_ref: encode_binary(target_ref), + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + message: message + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::PreReceiveError, pre_receive_error + end + + response.commit_id + end + def user_merge_branch(user, source_sha, target_branch, message) request_enum = QueueEnumerator.new response_enum = GitalyClient.call( diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d125d3b7211..0d47deab951 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6746,9 +6746,15 @@ msgstr "" msgid "Sherlock Transactions" msgstr "" +msgid "Show all activity" +msgstr "" + msgid "Show command" msgstr "" +msgid "Show comments only" +msgstr "" + msgid "Show complete raw log" msgstr "" @@ -8657,6 +8663,9 @@ msgstr "" msgid "You'll need to use different branch names to get a valid comparison." msgstr "" +msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options." +msgstr "" + msgid "You're receiving this email because %{reason}." msgstr "" diff --git a/package.json b/package.json index 4ec484a5e86..3d96f8b80d9 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "graphql": "^14.0.2", "imports-loader": "^0.8.0", "jed": "^1.1.1", + "jest-transform-graphql": "^2.1.0", "jquery": "^3.2.1", "jquery-ujs": "1.2.2", "jquery.waitforimages": "^2.2.0", diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 4cf14030ca1..82e24213408 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -1,18 +1,35 @@ require 'spec_helper' describe Admin::RunnersController do - let(:runner) { create(:ci_runner) } + let!(:runner) { create(:ci_runner) } before do sign_in(create(:admin)) end describe '#index' do + render_views + it 'lists all runners' do get :index expect(response).to have_gitlab_http_status(200) end + + it 'avoids N+1 queries', :request_store do + get :index + + control_count = ActiveRecord::QueryRecorder.new { get :index }.count + + create(:ci_runner, :tagged_only) + + # There is still an N+1 query for `runner.builds.count` + expect { get :index }.not_to exceed_query_limit(control_count + 1) + + expect(response).to have_gitlab_http_status(200) + expect(response.body).to have_content('tag1') + expect(response.body).to have_content('tag2') + end end describe '#show' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index c9e520317e8..dca74bd5f84 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -665,6 +665,14 @@ describe ApplicationController do expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store' end + + it 'does not set the "no-store" header for XHR requests' do + sign_in(user) + + get :index, xhr: true + + expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate' + end end end end diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index 307c5d60c57..8580900215c 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -112,7 +112,8 @@ describe IssuableCollections do assignee_username: 'user1', author_id: '2', author_username: 'user2', - authorized_only: 'true', + authorized_only: 'yes', + confidential: true, due_date: '2017-01-01', group_id: '3', iids: '4', @@ -140,6 +141,7 @@ describe IssuableCollections do 'assignee_username' => 'user1', 'author_id' => '2', 'author_username' => 'user2', + 'confidential' => true, 'label_name' => 'foo', 'milestone_title' => 'bar', 'my_reaction_emoji' => 'thumbsup', diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index aa97a417a98..36ce1119100 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -54,9 +54,9 @@ describe Projects::EnvironmentsController do it 'responds with a flat payload describing available environments' do expect(environments.count).to eq 3 - expect(environments.first['name']).to eq 'production' - expect(environments.second['name']).to eq 'staging/review-1' - expect(environments.third['name']).to eq 'staging/review-2' + expect(environments.first).to include('name' => 'production', 'name_without_type' => 'production') + expect(environments.second).to include('name' => 'staging/review-1', 'name_without_type' => 'review-1') + expect(environments.third).to include('name' => 'staging/review-2', 'name_without_type' => 'review-2') expect(json_response['available_count']).to eq 3 expect(json_response['stopped_count']).to eq 1 end @@ -155,9 +155,9 @@ describe Projects::EnvironmentsController do expect(response).to be_ok expect(response).not_to render_template 'folder' expect(json_response['environments'][0]) - .to include('name' => 'staging-1.0/review') + .to include('name' => 'staging-1.0/review', 'name_without_type' => 'review') expect(json_response['environments'][1]) - .to include('name' => 'staging-1.0/zzz') + .to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz') end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7ef34d773b..30d3b22d868 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -313,6 +313,20 @@ FactoryBot.define do end end + factory :youtrack_project, parent: :project do + has_external_issue_tracker true + + after :create do |project| + project.create_youtrack_service( + active: true, + properties: { + 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack', + 'issues_url' => 'http://youtrack/issues/:id' + } + ) + end + end + factory :jira_project, parent: :project do has_external_issue_tracker true jira_service diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 0e296ab2109..096756f19cc 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -66,7 +66,7 @@ describe 'Dropdown hint', :js do it 'filters with text' do filtered_search.set('a') - expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4) + expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5) end end @@ -119,6 +119,15 @@ describe 'Dropdown hint', :js do expect_tokens([{ name: 'my-reaction' }]) expect_filtered_search_input_empty end + + it 'opens the yes-no dropdown when you click on confidential' do + click_hint('confidential') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-confidential', visible: true) + expect_tokens([{ name: 'confidential' }]) + expect_filtered_search_input_empty + end end describe 'selecting from dropdown with some input' do diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 8abab3f35d6..da23aea1fc9 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -86,7 +86,7 @@ describe 'Search bar', :js do expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: original_size) end - it 'resets the dropdown filters' do + it 'resets the dropdown filters', :quarantine do filtered_search.click hint_offset = get_left_style(find('#js-dropdown-hint')['style']) @@ -100,7 +100,7 @@ describe 'Search bar', :js do find('.filtered-search-box .clear-search').click filtered_search.click - expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5) + expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6) expect(get_left_style(find('#js-dropdown-hint')['style'])).to eq(hint_offset) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c22ad0d20ef..986f3823275 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,7 +278,7 @@ describe 'GFM autocomplete', :js do end end - # This context has jsut one example in each contexts in order to improve spec performance. + # This context has just one example in each contexts in order to improve spec performance. context 'labels', :quarantine do let!(:backend) { create(:label, project: project, title: 'backend') } let!(:bug) { create(:label, project: project, title: 'bug') } diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 6e6c299ee2e..1522a3361a1 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -77,7 +77,7 @@ describe 'Editing file blob', :js do click_link 'Preview' wait_for_requests - # the above generates two seperate lists (not embedded) in CommonMark + # the above generates two separate lists (not embedded) in CommonMark expect(page).to have_content("sublist") expect(page).not_to have_xpath("//ol//li//ul") end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 72faeba92ee..f564ae34f11 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -212,7 +212,7 @@ describe 'Pages' do it 'tries to change the setting' do visit project_pages_path(project) - expect(page).to have_content("Force domains with SSL certificates to use HTTPS") + expect(page).to have_content("Force HTTPS (requires valid certificates)") uncheck :project_pages_https_only @@ -261,7 +261,7 @@ describe 'Pages' do visit project_pages_path(project) expect(page).not_to have_field(:project_pages_https_only) - expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS') + expect(page).not_to have_content('Force HTTPS (requires valid certificates)') expect(page).not_to have_button('Save') end end diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb index 7cd5b12802b..74b9a2b20cd 100644 --- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -6,11 +6,17 @@ describe 'User activates issue tracker', :js do let(:url) { 'http://tracker.example.com' } - def fill_form(active = true) + def fill_short_form(active = true) check 'Active' if active fill_in 'service_project_url', with: url fill_in 'service_issues_url', with: "#{url}/:id" + end + + def fill_full_form(active = true) + fill_short_form(active) + check 'Active' if active + fill_in 'service_new_issue_url', with: url end @@ -21,14 +27,20 @@ describe 'User activates issue tracker', :js do visit project_settings_integrations_path(project) end - shared_examples 'external issue tracker activation' do |tracker:| + shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false| describe 'user sets and activates the Service' do context 'when the connection test succeeds' do before do stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests end @@ -50,7 +62,13 @@ describe 'User activates issue tracker', :js do stub_request(:head, url).to_raise(HTTParty::Error) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests @@ -69,7 +87,13 @@ describe 'User activates issue tracker', :js do describe 'user sets the service but keeps it disabled' do before do click_link(tracker) - fill_form(false) + + if skip_new_issue_url + fill_short_form(false) + else + fill_full_form(false) + end + click_button('Save changes') end @@ -87,6 +111,7 @@ describe 'User activates issue tracker', :js do end it_behaves_like 'external issue tracker activation', tracker: 'Redmine' + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack', skip_new_issue_url: true it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' end diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb new file mode 100644 index 00000000000..bb6a030c1cf --- /dev/null +++ b/spec/features/projects/services/user_activates_youtrack_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'User activates issue tracker', :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:url) { 'http://tracker.example.com' } + + def fill_form(active = true) + check 'Active' if active + + fill_in 'service_project_url', with: url + fill_in 'service_issues_url', with: "#{url}/:id" + end + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_settings_integrations_path(project) + end + + shared_examples 'external issue tracker activation' do |tracker:| + describe 'user sets and activates the Service' do + context 'when the connection test succeeds' do + before do + stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + end + + it 'activates the service' do + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'shows the link in the menu' do + page.within('.nav-sidebar') do + expect(page).to have_link(tracker, href: url) + end + end + end + + context 'when the connection test fails' do + it 'activates the service' do + stub_request(:head, url).to_raise(HTTParty::Error) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(find('.flash-container-page')).to have_content 'Test failed.' + expect(find('.flash-container-page')).to have_content 'Save anyway' + + find('.flash-alert .flash-action').click + wait_for_requests + + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + end + end + + describe 'user sets the service but keeps it disabled' do + before do + click_link(tracker) + fill_form(false) + click_button('Save changes') + end + + it 'saves but does not activate the service' do + expect(page).to have_content("#{tracker} settings saved, but not activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'does not show the external tracker link in the menu' do + page.within('.nav-sidebar') do + expect(page).not_to have_link(tracker, href: url) + end + end + end + end + + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack' +end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 49244c53a91..49058d1372a 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -170,7 +170,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do fill_in :wiki_content, with: "1. one\n - sublist\n" click_on "Preview" - # the above generates two seperate lists (not embedded) in CommonMark + # the above generates two separate lists (not embedded) in CommonMark expect(page).to have_content("sublist") expect(page).not_to have_xpath("//ol//li//ul") end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index fe8000e419b..47e2548c3d6 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -490,6 +490,32 @@ describe IssuesFinder do end end + context 'filtering by confidential' do + set(:confidential_issue) { create(:issue, project: project1, confidential: true) } + + context 'no filtering' do + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, confidential_issue) + end + end + + context 'user filters confidential issues' do + let(:params) { { confidential: true } } + + it 'returns only confdential issues' do + expect(issues).to contain_exactly(confidential_issue) + end + end + + context 'user filters only public issues' do + let(:params) { { confidential: false } } + + it 'returns only confdential issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } @@ -556,7 +582,7 @@ describe IssuesFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(4) + expect(finder.row_count).to eq(5) end it 'returns the number of rows for a given state' do diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json index f1d33e3ce7b..9a10ab18c30 100644 --- a/spec/fixtures/api/schemas/environment.json +++ b/spec/fixtures/api/schemas/environment.json @@ -20,6 +20,7 @@ "state": { "type": "string" }, "external_url": { "$ref": "types/nullable_string.json" }, "environment_type": { "$ref": "types/nullable_string.json" }, + "name_without_type": { "type": "string" }, "has_stop_action": { "type": "boolean" }, "environment_path": { "type": "string" }, "stop_path": { "type": "string" }, diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js new file mode 100644 index 00000000000..08d725cd4e4 --- /dev/null +++ b/spec/frontend/__mocks__/file_mock.js @@ -0,0 +1 @@ +export default ''; diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js index 7bb8e26b81a..7bb8e26b81a 100644 --- a/spec/javascripts/issuable_suggestions/components/app_spec.js +++ b/spec/frontend/issuable_suggestions/components/app_spec.js diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js index 7bd1fe678f4..7bd1fe678f4 100644 --- a/spec/javascripts/issuable_suggestions/components/item_spec.js +++ b/spec/frontend/issuable_suggestions/components/item_spec.js diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/frontend/issuable_suggestions/mock_data.js index 4f0f9ef8d62..4f0f9ef8d62 100644 --- a/spec/javascripts/issuable_suggestions/mock_data.js +++ b/spec/frontend/issuable_suggestions/mock_data.js diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 52895f35f3a..ecd28594873 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -31,4 +31,224 @@ describe('Environment table', () => { expect(vm.$el.getAttribute('class')).toContain('ci-table'); }); + + describe('sortEnvironments', () => { + it('should sort environments by last updated', () => { + const mockItems = [ + { + name: 'old', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 0, 5).toISOString(), + }, + }, + { + name: 'new', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 1, 5).toISOString(), + }, + }, + { + name: 'older', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2018, 0, 5).toISOString(), + }, + }, + { + name: 'an environment with no deployment', + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older, noDeploy] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([newer, old, older, noDeploy]); + }); + + it('should push environments with no deployments to the bottom', () => { + const mockItems = [ + { + name: 'production', + size: 1, + id: 2, + state: 'available', + external_url: 'https://google.com/production', + environment_type: null, + last_deployment: null, + has_stop_action: false, + environment_path: '/Commit451/lab-coat/environments/2', + stop_path: '/Commit451/lab-coat/environments/2/stop', + folder_path: '/Commit451/lab-coat/environments/folders/production', + created_at: '2019-01-17T16:26:10.064Z', + updated_at: '2019-01-17T16:27:37.717Z', + can_stop: true, + }, + { + name: 'review/225addcibuildstatus', + size: 2, + isFolder: true, + isLoadingFolderContent: false, + folderName: 'review', + isOpen: false, + children: [], + id: 12, + state: 'available', + external_url: 'https://google.com/review/225addcibuildstatus', + environment_type: 'review', + last_deployment: null, + has_stop_action: false, + environment_path: '/Commit451/lab-coat/environments/12', + stop_path: '/Commit451/lab-coat/environments/12/stop', + folder_path: '/Commit451/lab-coat/environments/folders/review', + created_at: '2019-01-17T16:27:37.877Z', + updated_at: '2019-01-17T16:27:37.883Z', + can_stop: true, + }, + { + name: 'staging', + size: 1, + id: 1, + state: 'available', + external_url: 'https://google.com/staging', + environment_type: null, + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + scheduled_actions: [], + }, + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [prod, review, staging] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([review, staging, prod]); + }); + + it('should sort environments by folder first', () => { + const mockItems = [ + { + name: 'old', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 0, 5).toISOString(), + }, + }, + { + name: 'new', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 1, 5).toISOString(), + }, + }, + { + name: 'older', + size: 3, + isFolder: true, + children: [], + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]); + }); + + it('should break ties by name', () => { + const mockItems = [ + { + name: 'old', + isFolder: false, + }, + { + name: 'new', + isFolder: false, + }, + { + folderName: 'older', + isFolder: true, + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]); + }); + }); + + describe('sortedEnvironments', () => { + it('it should sort children as well', () => { + const mockItems = [ + { + name: 'production', + last_deployment: null, + }, + { + name: 'review/225addcibuildstatus', + isFolder: true, + folderName: 'review', + isOpen: true, + children: [ + { + name: 'review/225addcibuildstatus', + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + }, + }, + { + name: 'review/master', + last_deployment: { + created_at: '2019-02-17T16:26:15.125Z', + }, + }, + ], + }, + { + name: 'staging', + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + }, + }, + ]; + const [production, review, staging] = mockItems; + const [addcibuildstatus, master] = mockItems[1].children; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + expect(vm.sortedEnvironments.map(env => env.name)).toEqual([ + review.name, + staging.name, + production.name, + ]); + + expect(vm.sortedEnvironments[0].children).toEqual([master, addcibuildstatus]); + }); + }); }); diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js new file mode 100644 index 00000000000..52d2e7ce947 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_filter_note_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; +import eventHub from '~/notes/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('DiscussionFilterNote component', () => { + let vm; + + const createComponent = () => { + const Component = Vue.extend(DiscussionFilterNote); + + return mountComponent(Component); + }; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('timelineContent', () => { + it('returns string containing instruction for switching feed type', () => { + expect(vm.timelineContent).toBe( + "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.", + ); + }); + }); + }); + + describe('methods', () => { + describe('selectFilter', () => { + it('emits `dropdownSelect` event on `eventHub` with provided param', () => { + spyOn(eventHub, '$emit'); + + vm.selectFilter(1); + + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); + }); + }); + }); + + describe('template', () => { + it('renders component container element', () => { + expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); + }); + + it('renders comment icon element', () => { + expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( + 'comment', + ); + }); + + it('renders filter information note', () => { + expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('renders filter buttons', () => { + const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); + + expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( + 'Show all activity', + ); + + expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( + 'Show comments only', + ); + }); + + it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(0); + }); + + it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 91dab58ba7f..1c366aee8e2 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import createStore from '~/notes/stores'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { discussionFiltersMock, discussionMock } from '../mock_data'; @@ -54,14 +54,18 @@ describe('DiscussionFilter component', () => { }); it('updates to the selected item', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); filterItem.click(); expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim()); }); it('only updates when selected filter changes', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); spyOn(vm, 'filterDiscussion'); filterItem.click(); @@ -70,21 +74,27 @@ describe('DiscussionFilter component', () => { }); it('disables commenting when "Show history only" filter is applied', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); filterItem.click(); expect(vm.$store.state.commentsDisabled).toBe(true); }); it('enables commenting when "Show history only" filter is not applied', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); filterItem.click(); expect(vm.$store.state.commentsDisabled).toBe(false); }); it('renders a dropdown divider for the default filter', () => { - const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + const defaultFilter = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`, + ); expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 82f58dafc78..d716ece3766 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -126,6 +126,13 @@ describe('note_app', () => { expect(wrapper.find('.js-main-target-form').exists()).toBe(false); }); + it('should render discussion filter note `commentsDisabled` is true', () => { + store.state.commentsDisabled = true; + wrapper = mountComponent(); + + expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); + }); + it('should render form comment button as disabled', () => { expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); }); diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a0270d93d50..43222ddb5e2 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -121,6 +121,42 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do end end + context "youtrack project" do + let(:project) { create(:youtrack_project) } + + before do + project.update!(issues_enabled: false) + end + + context "with right markdown" do + let(:issue) { ExternalIssue.new("YT-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with underscores in the prefix" do + let(:issue) { ExternalIssue.new("PRJ_1-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with lowercase letters in the prefix" do + let(:issue) { ExternalIssue.new("YTkPrj-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("T-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + end + context "jira project" do let(:project) { create(:jira_project) } let(:reference) { issue.to_reference } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8a9e78ba3c3..b3a728c139e 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1704,6 +1704,37 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#merge_to_ref' do + let(:repository) { mutable_repository } + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:right_branch) { 'test-master' } + let(:target_ref) { 'refs/merge-requests/999/merge' } + + before do + repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + end + + def merge_to_ref + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + end + + it 'generates a commit in the target_ref' do + expect(repository.ref_exists?(target_ref)).to be(false) + + commit_sha = merge_to_ref + ref_head = repository.commit(target_ref) + + expect(commit_sha).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(commit_sha) + end + + it 'does not change the right branch HEAD' do + expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + end + end + describe '#merge' do let(:repository) { mutable_repository } let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index f2eccec4635..018a5d3dd3d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -233,6 +233,7 @@ project: - pushover_service - jira_service - redmine_service +- youtrack_service - custom_issue_tracker_service - bugzilla_service - gitlab_issue_tracker_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1327f414498..773651dd226 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6630,6 +6630,26 @@ "deploy_keys": [], "services": [ { + "id": 101, + "title": "YouTrack", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.327Z", + "updated_at": "2016-06-14T15:01:51.327Z", + "active": false, + "properties": {}, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "job_events": true, + "type": "YoutrackService", + "category": "issue_tracker", + "default": false, + "wiki_page_events": true + }, + { "id": 100, "title": "JetBrains TeamCity CI", "project_id": 5, diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 8bb0c1a0b8a..9f2214f7ce7 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe Gitlab::Profiler do - RSpec::Matchers.define_negated_matcher :not_change, :change - let(:null_logger) { Logger.new('/dev/null') } let(:private_token) { 'private' } @@ -187,7 +185,7 @@ describe Gitlab::Profiler do end it 'does not modify the standard Rails loggers' do - expect { described_class.with_custom_logger(nil) { } } + expect { described_class.with_custom_logger(nil) {} } .to not_change { ActiveRecord::Base.logger } .and not_change { ActionController::Base.logger } .and not_change { ActiveSupport::LogSubscriber.colorize_logging } @@ -204,7 +202,7 @@ describe Gitlab::Profiler do end it 'cleans up ApplicationController afterwards' do - expect { described_class.with_user(user) { } } + expect { described_class.with_user(user) {} } .to not_change { ActionController.instance_methods(false) } end end @@ -213,7 +211,7 @@ describe Gitlab::Profiler do it 'does not define methods on ApplicationController' do expect(ApplicationController).not_to receive(:define_method) - described_class.with_user(nil) { } + described_class.with_user(nil) {} end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b9567ab4d65..7ea701dd035 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::Pipeline, :mailer do + include ProjectForksHelper + let(:user) { create(:user) } set(:project) { create(:project) } @@ -2114,66 +2116,81 @@ describe Ci::Pipeline, :mailer do describe "#all_merge_requests" do let(:project) { create(:project) } - let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') } - it "returns all merge requests having the same source branch" do - merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) + shared_examples 'a method that returns all merge requests for a given pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') } - expect(pipeline.all_merge_requests).to eq([merge_request]) - end + it "returns all merge requests having the same source branch" do + merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref) - it "doesn't return merge requests having a different source branch" do - create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') - - expect(pipeline.all_merge_requests).to be_empty - end - - context 'when there is a merge request pipeline' do - let(:source_branch) { 'feature' } - let(:target_branch) { 'master' } - - let!(:pipeline) do - create(:ci_pipeline, - source: :merge_request, - project: project, - ref: source_branch, - merge_request: merge_request) + expect(pipeline.all_merge_requests).to eq([merge_request]) end - let(:merge_request) do - create(:merge_request, - source_project: project, - source_branch: source_branch, - target_project: project, - target_branch: target_branch) - end + it "doesn't return merge requests having a different source branch" do + create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: 'feature', target_branch: 'master') - it 'returns an associated merge request' do - expect(pipeline.all_merge_requests).to eq([merge_request]) + expect(pipeline.all_merge_requests).to be_empty end - context 'when there is another merge request pipeline that targets a different branch' do - let(:target_branch_2) { 'merge-test' } + context 'when there is a merge request pipeline' do + let(:source_branch) { 'feature' } + let(:target_branch) { 'master' } - let!(:pipeline_2) do + let!(:pipeline) do create(:ci_pipeline, source: :merge_request, - project: project, + project: pipeline_project, ref: source_branch, - merge_request: merge_request_2) + merge_request: merge_request) end - let(:merge_request_2) do + let(:merge_request) do create(:merge_request, - source_project: project, + source_project: pipeline_project, source_branch: source_branch, target_project: project, - target_branch: target_branch_2) + target_branch: target_branch) end - it 'does not return an associated merge request' do - expect(pipeline.all_merge_requests).not_to include(merge_request_2) + it 'returns an associated merge request' do + expect(pipeline.all_merge_requests).to eq([merge_request]) end + + context 'when there is another merge request pipeline that targets a different branch' do + let(:target_branch_2) { 'merge-test' } + + let!(:pipeline_2) do + create(:ci_pipeline, + source: :merge_request, + project: pipeline_project, + ref: source_branch, + merge_request: merge_request_2) + end + + let(:merge_request_2) do + create(:merge_request, + source_project: pipeline_project, + source_branch: source_branch, + target_project: project, + target_branch: target_branch_2) + end + + it 'does not return an associated merge request' do + expect(pipeline.all_merge_requests).not_to include(merge_request_2) + end + end + end + end + + it_behaves_like 'a method that returns all merge requests for a given pipeline' do + let(:pipeline_project) { project } + end + + context 'for a fork' do + let(:fork) { fork_project(project) } + + it_behaves_like 'a method that returns all merge requests for a given pipeline' do + let(:pipeline_project) { fork } end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 2d554326f05..ab1b306e597 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -164,6 +164,28 @@ describe Environment do end end + describe '#name_without_type' do + context 'when it is inside a folder' do + subject(:environment) do + create(:environment, name: 'staging/review-1') + end + + it 'returns name without folder' do + expect(environment.name_without_type).to eq 'review-1' + end + end + + context 'when the environment if a top-level item itself' do + subject(:environment) do + create(:environment, name: 'production') + end + + it 'returns full name' do + expect(environment.name_without_type).to eq 'production' + end + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 5d18e085a6f..6101df2e099 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -765,6 +765,15 @@ describe Issue do end end + describe '.confidential_only' do + it 'only returns confidential_only issues' do + create(:issue) + confidential_issue = create(:issue, confidential: true) + + expect(described_class.confidential_only).to eq([confidential_issue]) + end + end + it_behaves_like 'throttled touch' do subject { create(:issue, updated_at: 1.hour.ago) } end diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb new file mode 100644 index 00000000000..9524b526a46 --- /dev/null +++ b/spec/models/project_services/youtrack_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe YoutrackService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + end + end + + describe '.reference_pattern' do + it_behaves_like 'allows project key on reference pattern' + + it 'does allow project prefix on the reference' do + expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123') + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bcbe687f4a2..9cc9894003d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -50,6 +50,7 @@ describe Project do it { is_expected.to have_one(:teamcity_service) } it { is_expected.to have_one(:jira_service) } it { is_expected.to have_one(:redmine_service) } + it { is_expected.to have_one(:youtrack_service) } it { is_expected.to have_one(:custom_issue_tracker_service) } it { is_expected.to have_one(:bugzilla_service) } it { is_expected.to have_one(:gitlab_issue_tracker_service) } @@ -2511,6 +2512,16 @@ describe Project do end end + describe '#set_repository_writable!' do + it 'sets repository_read_only to false' do + project = create(:project, :read_only) + + expect { project.set_repository_writable! } + .to change(project, :repository_read_only) + .from(true).to(false) + end + end + describe '#pushes_since_gc' do let(:project) { create(:project) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f78760bf567..17201d8b90a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1373,6 +1373,29 @@ describe Repository do end end + describe '#merge_to_ref' do + let(:merge_request) do + create(:merge_request, source_branch: 'feature', + target_branch: 'master', + source_project: project) + end + + it 'writes merge of source and target to MR merge_ref_path' do + merge_commit_id = repository.merge_to_ref(user, + merge_request.diff_head_sha, + merge_request, + merge_request.merge_ref_path, + 'Custom message') + + merge_commit = repository.commit(merge_commit_id) + + expect(merge_commit.message).to eq('Custom message') + expect(merge_commit.author_name).to eq(user.name) + expect(merge_commit.author_email).to eq(user.commit_email) + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + end + describe '#ff_merge' do before do repository.add_branch(user, 'ff-target', 'feature~5') diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index d10ee6cc320..01bab2a1361 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -183,6 +183,18 @@ describe API::Issues do expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end + it 'returns only confidential issues' do + get api('/issues', user), params: { confidential: true, scope: 'all' } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api('/issues', user), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + it 'returns issues reacted by the authenticated user' do issue2 = create(:issue, project: project, author: user, assignees: [user]) create(:award_emoji, awardable: issue2, user: user2, name: 'star') @@ -354,7 +366,7 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api("/issues", user), params: { iids: [99999] } + get api("/issues", user), params: { iids: [0] } expect_paginated_array_response([]) end @@ -557,6 +569,18 @@ describe API::Issues do expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) end + it 'returns only confidential issues' do + get api(base_url, user), params: { confidential: true } + + expect_paginated_array_response(group_confidential_issue.id) + end + + it 'returns only public issues' do + get api(base_url, user), params: { confidential: false } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + it 'returns an array of labeled group issues' do get api(base_url, user), params: { labels: group_label.title } @@ -603,7 +627,7 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api(base_url, user), params: { iids: [99999] } + get api(base_url, user), params: { iids: [0] } expect_paginated_array_response([]) end @@ -782,6 +806,18 @@ describe API::Issues do expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end + it 'returns only confidential issues' do + get api("#{base_url}/issues", author), params: { confidential: true } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api("#{base_url}/issues", author), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) @@ -837,7 +873,7 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api("#{base_url}/issues", user), params: { iids: [99999] } + get api("#{base_url}/issues", user), params: { iids: [0] } expect_paginated_array_response([]) end @@ -1873,7 +1909,7 @@ describe API::Issues do end it "returns 404 when issue doesn't exists" do - get api("/projects/#{project.id}/issues/9999/closed_by", user) + get api("/projects/#{project.id}/issues/0/closed_by", user) expect(response).to have_gitlab_http_status(404) end @@ -1958,7 +1994,7 @@ describe API::Issues do end it "returns 404 when issue doesn't exists" do - get_related_merge_requests(project.id, 999999, user) + get_related_merge_requests(project.id, 0, user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index 3c4719964b6..f37d84fddef 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -16,7 +16,7 @@ describe API::Keys do context 'when authenticated' do it 'returns 404 for non-existing key' do - get api('/keys/999999', admin) + get api('/keys/0', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 Not found') end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index 6530dc956cb..8a67d98fc4c 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -30,7 +30,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/versions", user) + get api("/projects/#{project.id}/merge_requests/0/versions", user) expect(response).to have_gitlab_http_status(404) end end @@ -53,7 +53,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do end it 'returns a 404 when merge_request version_id is not found' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b4cd3130dc5..db56739af2f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -441,7 +441,7 @@ describe API::MergeRequests do end it "returns a 404 error if merge_request_iid not found" do - get api("/projects/#{project.id}/merge_requests/999", user) + get api("/projects/#{project.id}/merge_requests/0", user) expect(response).to have_gitlab_http_status(404) end @@ -531,7 +531,7 @@ describe API::MergeRequests do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/commits", user) + get api("/projects/#{project.id}/merge_requests/0/commits", user) expect(response).to have_gitlab_http_status(404) end @@ -551,7 +551,7 @@ describe API::MergeRequests do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/changes", user) + get api("/projects/#{project.id}/merge_requests/0/changes", user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 145356c4df5..2e376109b42 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -149,7 +149,7 @@ describe API::Namespaces do context "when namespace doesn't exist" do it 'returns not-found' do - get api('/namespaces/9999', request_actor) + get api('/namespaces/0', request_actor) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 49b5dfb0b33..895f05a98e8 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -23,13 +23,13 @@ describe API::ProjectMilestones do end it 'returns 404 response when the project does not exists' do - delete api("/projects/999/milestones/#{milestone.id}", user) + delete api("/projects/0/milestones/#{milestone.id}", user) expect(response).to have_gitlab_http_status(404) end it 'returns 404 response when the milestone does not exists' do - delete api("/projects/#{project.id}/milestones/999", user) + delete api("/projects/#{project.id}/milestones/0", user) expect(response).to have_gitlab_http_status(404) end @@ -49,4 +49,74 @@ describe API::ProjectMilestones do params: { state_event: 'close' } end end + + describe 'POST /projects/:id/milestones/:milestone_id/promote' do + let(:group) { create(:group) } + + before do + project.update(namespace: group) + end + + context 'when user does not have permission to promote milestone' do + before do + group.add_guest(user) + end + + it 'returns 403' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'when user has permission' do + before do + group.add_developer(user) + end + + it 'returns 200' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(200) + expect(group.milestones.first.title).to eq(milestone.title) + end + + it 'returns 200 for closed milestone' do + post api("/projects/#{project.id}/milestones/#{closed_milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(200) + expect(group.milestones.first.title).to eq(closed_milestone.title) + end + end + + context 'when no such resources' do + before do + group.add_developer(user) + end + + it 'returns 404 response when the project does not exist' do + post api("/projects/0/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 response when the milestone does not exist' do + post api("/projects/#{project.id}/milestones/0/promote", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when project does not belong to group' do + before do + project.update(namespace: user.namespace) + end + + it 'returns 403' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end end diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index ab5d4de7ff7..80e5033dab4 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -92,6 +92,22 @@ describe API::ProjectTemplates do expect(json_response['name']).to eq('Actionscript') end + it 'returns C++ gitignore' do + get api("/projects/#{public_project.id}/templates/gitignores/C++") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('C++') + end + + it 'returns C++ gitignore for URL-encoded names' do + get api("/projects/#{public_project.id}/templates/gitignores/C%2B%2B") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('C++') + end + it 'returns a specific gitlab_ci_yml' do get api("/projects/#{public_project.id}/templates/gitlab_ci_ymls/Android") @@ -125,6 +141,18 @@ describe API::ProjectTemplates do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/license') end + + shared_examples 'path traversal attempt' do |template_type| + it 'rejects invalid filenames' do + get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea") + + expect(response).to have_gitlab_http_status(500) + end + end + + TemplateFinder::VENDORED_TEMPLATES.each do |template_type, _| + it_behaves_like 'path traversal attempt', template_type + end end describe 'GET /projects/:id/templates/licenses/:key' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 792abdb2972..856fe1bbe89 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -778,7 +778,7 @@ describe API::Projects do let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) } it 'returns error when user not found' do - get api('/users/9999/projects/') + get api('/users/0/projects/') expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1385,7 +1385,7 @@ describe API::Projects do end it 'fails if forked_from project which does not exist' do - post api("/projects/#{project_fork_target.id}/fork/9999", admin) + post api("/projects/#{project_fork_target.id}/fork/0", admin) expect(response).to have_gitlab_http_status(404) end @@ -1936,7 +1936,7 @@ describe API::Projects do end it 'returns not_found(404) for not existing project' do - get api("/projects/9999999999/languages", user) + get api("/projects/0/languages", user) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 7f11c8c9fe8..5ca442bc448 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -241,7 +241,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - get api('/runners/9999', admin) + get api('/runners/0', admin) expect(response).to have_gitlab_http_status(404) end @@ -394,7 +394,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - update_runner(9999, admin, description: 'test') + update_runner(0, admin, description: 'test') expect(response).to have_gitlab_http_status(404) end @@ -468,7 +468,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - delete api('/runners/9999', admin) + delete api('/runners/0', admin) expect(response).to have_gitlab_http_status(404) end @@ -573,7 +573,7 @@ describe API::Runners do context "when runner doesn't exist" do it 'returns 404' do - get api('/runners/9999/jobs', admin) + get api('/runners/0/jobs', admin) expect(response).to have_gitlab_http_status(404) end @@ -626,7 +626,7 @@ describe API::Runners do context "when runner doesn't exist" do it 'returns 404' do - get api('/runners/9999/jobs', user) + get api('/runners/0/jobs', user) expect(response).to have_gitlab_http_status(404) end @@ -857,7 +857,7 @@ describe API::Runners do end it 'returns 404 is runner is not found' do - delete api("/projects/#{project.id}/runners/9999", user) + delete api("/projects/#{project.id}/runners/0", user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 831f47debeb..c48ca832c85 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -126,7 +126,7 @@ describe API::Search do context 'when group does not exist' do it 'returns 404 error' do - get api('/groups/9999/search', user), params: { scope: 'issues', search: 'awesome' } + get api('/groups/0/search', user), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(404) end @@ -222,7 +222,7 @@ describe API::Search do context 'when project does not exist' do it 'returns 404 error' do - get api('/projects/9999/search', user), params: { scope: 'issues', search: 'awesome' } + get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b381431306d..a879426589d 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -335,7 +335,7 @@ describe API::Users do end it "returns a 404 error if user id not found" do - get api("/users/9999", user) + get api("/users/0", user) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -732,7 +732,7 @@ describe API::Users do end it "returns 404 for non-existing user" do - put api("/users/999999", admin), params: { bio: 'update should fail' } + put api("/users/0", admin), params: { bio: 'update should fail' } expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -836,7 +836,7 @@ describe API::Users do end it "returns 400 for invalid ID" do - post api("/users/999999/keys", admin) + post api("/users/0/keys", admin) expect(response).to have_gitlab_http_status(400) end end @@ -895,7 +895,7 @@ describe API::Users do it 'returns 404 error if user not found' do user.keys << key user.save - delete api("/users/999999/keys/#{key.id}", admin) + delete api("/users/0/keys/#{key.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -930,7 +930,7 @@ describe API::Users do end it 'returns 400 for invalid ID' do - post api('/users/999999/gpg_keys', admin) + post api('/users/0/gpg_keys', admin) expect(response).to have_gitlab_http_status(400) end @@ -951,7 +951,7 @@ describe API::Users do context 'when authenticated' do it 'returns 404 for non-existing user' do - get api('/users/999999/gpg_keys', admin) + get api('/users/0/gpg_keys', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1007,7 +1007,7 @@ describe API::Users do user.keys << key user.save - delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin) + delete api("/users/0/gpg_keys/#{gpg_key.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1051,7 +1051,7 @@ describe API::Users do user.gpg_keys << gpg_key user.save - post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin) + post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1089,7 +1089,7 @@ describe API::Users do end it "returns a 400 for invalid ID" do - post api("/users/999999/emails", admin) + post api("/users/0/emails", admin) expect(response).to have_gitlab_http_status(400) end @@ -1121,7 +1121,7 @@ describe API::Users do context 'when authenticated' do it 'returns 404 for non-existing user' do - get api('/users/999999/emails', admin) + get api('/users/0/emails', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1177,7 +1177,7 @@ describe API::Users do it 'returns 404 error if user not found' do user.emails << email user.save - delete api("/users/999999/emails/#{email.id}", admin) + delete api("/users/0/emails/#{email.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1227,7 +1227,7 @@ describe API::Users do end it "returns 404 for non-existing user" do - perform_enqueued_jobs { delete api("/users/999999", admin) } + perform_enqueued_jobs { delete api("/users/0", admin) } expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1778,7 +1778,7 @@ describe API::Users do end it 'returns a 404 error if user id not found' do - post api('/users/9999/block', admin) + post api('/users/0/block', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1816,7 +1816,7 @@ describe API::Users do end it 'returns a 404 error if user id not found' do - post api('/users/9999/block', admin) + post api('/users/0/block', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb index 3f621ed5944..cbdef008b07 100644 --- a/spec/services/clusters/applications/create_service_spec.rb +++ b/spec/services/clusters/applications/create_service_spec.rb @@ -26,12 +26,6 @@ describe Clusters::Applications::CreateService do end.to change(cluster, :application_helm) end - it 'schedules an install via worker' do - expect(ClusterInstallAppWorker).to receive(:perform_async).with('helm', anything).once - - subject - end - context 'application already installed' do let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) } @@ -42,88 +36,101 @@ describe Clusters::Applications::CreateService do end it 'schedules an upgrade for the application' do - expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original + expect(ClusterUpgradeAppWorker).to receive(:perform_async) subject end end - context 'cert manager application' do - let(:params) do - { - application: 'cert_manager', - email: 'test@example.com' - } - end - + context 'known applications' do before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + create(:clusters_applications_helm, :installed, cluster: cluster) end - it 'creates the application' do - expect do - subject + context 'cert manager application' do + let(:params) do + { + application: 'cert_manager', + email: 'test@example.com' + } + end - cluster.reload - end.to change(cluster, :application_cert_manager) - end + before do + expect_any_instance_of(Clusters::Applications::CertManager) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the email' do - expect(subject.email).to eq('test@example.com') - end - end + it 'creates the application' do + expect do + subject - context 'jupyter application' do - let(:params) do - { - application: 'jupyter', - hostname: 'example.com' - } - end + cluster.reload + end.to change(cluster, :application_cert_manager) + end - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + it 'sets the email' do + expect(subject.email).to eq('test@example.com') + end end - it 'creates the application' do - expect do - subject + context 'jupyter application' do + let(:params) do + { + application: 'jupyter', + hostname: 'example.com' + } + end - cluster.reload - end.to change(cluster, :application_jupyter) - end + before do + create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster) + expect_any_instance_of(Clusters::Applications::Jupyter) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the hostname' do - expect(subject.hostname).to eq('example.com') - end + it 'creates the application' do + expect do + subject - it 'sets the oauth_application' do - expect(subject.oauth_application).to be_present - end - end + cluster.reload + end.to change(cluster, :application_jupyter) + end - context 'knative application' do - let(:params) do - { - application: 'knative', - hostname: 'example.com' - } - end + it 'sets the hostname' do + expect(subject.hostname).to eq('example.com') + end - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + it 'sets the oauth_application' do + expect(subject.oauth_application).to be_present + end end - it 'creates the application' do - expect do - subject + context 'knative application' do + let(:params) do + { + application: 'knative', + hostname: 'example.com' + } + end - cluster.reload - end.to change(cluster, :application_knative) - end + before do + expect_any_instance_of(Clusters::Applications::Knative) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the hostname' do - expect(subject.hostname).to eq('example.com') + it 'creates the application' do + expect do + subject + + cluster.reload + end.to change(cluster, :application_knative) + end + + it 'sets the hostname' do + expect(subject.hostname).to eq('example.com') + end end end @@ -140,19 +147,21 @@ describe Clusters::Applications::CreateService do using RSpec::Parameterized::TableSyntax - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) - end - - where(:application, :association, :allowed) do - 'helm' | :application_helm | true - 'ingress' | :application_ingress | true - 'runner' | :application_runner | false - 'jupyter' | :application_jupyter | false - 'prometheus' | :application_prometheus | false + where(:application, :association, :allowed, :pre_create_helm) do + 'helm' | :application_helm | true | false + 'ingress' | :application_ingress | true | true + 'runner' | :application_runner | false | true + 'jupyter' | :application_jupyter | false | true + 'prometheus' | :application_prometheus | false | true end with_them do + before do + klass = "Clusters::Applications::#{application.titleize}" + allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original + create(:clusters_applications_helm, :installed, cluster: cluster) if pre_create_helm + end + let(:params) { { application: application } } it 'executes for each application' do @@ -168,5 +177,68 @@ describe Clusters::Applications::CreateService do end end end + + context 'when application is installable' do + shared_examples 'installable applications' do + it 'makes the application scheduled' do + expect do + subject + end.to change { Clusters::Applications::Helm.with_status(:scheduled).count }.by(1) + end + + it 'schedules an install via worker' do + expect(ClusterInstallAppWorker) + .to receive(:perform_async) + .with(*worker_arguments) + .once + + subject + end + end + + context 'when application is associated with a cluster' do + let(:application) { create(:clusters_applications_helm, :installable, cluster: cluster) } + let(:worker_arguments) { [application.name, application.id] } + + it_behaves_like 'installable applications' + end + + context 'when application is not associated with a cluster' do + let(:worker_arguments) { [params[:application], kind_of(Numeric)] } + + it_behaves_like 'installable applications' + end + end + + context 'when installation is already in progress' do + let!(:application) { create(:clusters_applications_helm, :installing, cluster: cluster) } + + it 'raises an exception' do + expect { subject } + .to raise_exception(StateMachines::InvalidTransition) + .and not_change(application.class.with_status(:scheduled), :count) + end + + it 'does not schedule a cluster worker' do + expect(ClusterInstallAppWorker).not_to receive(:perform_async) + end + end + + context 'when application is installed' do + %i(installed updated).each do |status| + let(:application) { create(:clusters_applications_helm, status, cluster: cluster) } + + it 'schedules an upgrade via worker' do + expect(ClusterUpgradeAppWorker) + .to receive(:perform_async) + .with(application.name, application.id) + .once + + subject + + expect(application.reload).to be_scheduled + end + end + end end end diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb deleted file mode 100644 index 8380932dfaa..00000000000 --- a/spec/services/clusters/applications/schedule_installation_service_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe Clusters::Applications::ScheduleInstallationService do - def count_scheduled - application&.class&.with_status(:scheduled)&.count || 0 - end - - shared_examples 'a failing service' do - it 'raise an exception' do - expect(ClusterInstallAppWorker).not_to receive(:perform_async) - count_before = count_scheduled - - expect { service.execute }.to raise_error(StandardError) - expect(count_scheduled).to eq(count_before) - end - end - - describe '#execute' do - let(:service) { described_class.new(application) } - - context 'when application is installable' do - let(:application) { create(:clusters_applications_helm, :installable) } - - it 'make the application scheduled' do - expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once - - expect { service.execute }.to change { application.class.with_status(:scheduled).count }.by(1) - end - end - - context 'when installation is already in progress' do - let(:application) { create(:clusters_applications_helm, :installing) } - - it_behaves_like 'a failing service' - end - - context 'when application is nil' do - let(:application) { nil } - - it_behaves_like 'a failing service' - end - - context 'when application cannot be persisted' do - let(:application) { create(:clusters_applications_helm) } - - before do - expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid) - end - - it_behaves_like 'a failing service' - end - - context 'when application is installed' do - let(:application) { create(:clusters_applications_helm, :installed) } - - it 'schedules an upgrade via worker' do - expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once - - service.execute - - expect(application).to be_scheduled - end - end - - context 'when application is updated' do - let(:application) { create(:clusters_applications_helm, :updated) } - - it 'schedules an upgrade via worker' do - expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once - - service.execute - - expect(application).to be_scheduled - end - end - end -end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 04a62aa454d..ede79b87bcc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -224,6 +224,18 @@ describe MergeRequests::MergeService do expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end + it 'logs and saves error if user is not authorized' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + service.execute(merge_request) + + expect(merge_request.merge_error) + .to eq('You are not allowed to merge this merge request') + end + it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb new file mode 100644 index 00000000000..96f2fde7117 --- /dev/null +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MergeToRefService do + shared_examples_for 'MergeService for target ref' do + it 'target_ref has the same state of target branch' do + repo = merge_request.target_project.repository + + process_merge_to_ref + merge_service.execute(merge_request) + + ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3) + target_branch_commits = repo.commits(merge_request.target_branch, limit: 3) + + ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit| + expect(ref_commit.parents).to eq(target_branch_commit.parents) + end + end + end + + set(:user) { create(:user) } + let(:merge_request) { create(:merge_request, :simple) } + let(:project) { merge_request.project } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) do + described_class.new(project, user, + commit_message: 'Awesome message', + 'should_remove_source_branch' => true) + end + + def process_merge_to_ref + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + it 'writes commit to merge ref' do + repository = project.repository + target_ref = merge_request.merge_ref_path + + expect(repository.ref_exists?(target_ref)).to be(false) + + result = service.execute(merge_request) + + ref_head = repository.commit(target_ref) + + expect(result[:status]).to eq(:success) + expect(result[:commit_id]).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(result[:commit_id]) + end + + it 'does not send any mail' do + expect { process_merge_to_ref }.not_to change { ActionMailer::Base.deliveries.count } + end + + it 'does not change the MR state' do + expect { process_merge_to_ref }.not_to change { merge_request.state } + end + + it 'does not create notes' do + expect { process_merge_to_ref }.not_to change { merge_request.notes.count } + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + + process_merge_to_ref + end + + it 'returns error when feature is disabled' do + stub_feature_flags(merge_to_tmp_merge_ref_path: false) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Feature is not enabled') + end + + it 'returns an error when the failing to process the merge' do + allow(project.repository).to receive(:merge_to_ref).and_return(nil) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Conflicts detected during merge') + end + + context 'commit history comparison with regular MergeService' do + let(:merge_ref_service) do + described_class.new(project, user, {}) + end + + let(:merge_service) do + MergeRequests::MergeService.new(project, user, {}) + end + + context 'when merge commit' do + it_behaves_like 'MergeService for target ref' + end + + context 'when merge commit with squash' do + before do + merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature') + end + + it_behaves_like 'MergeService for target ref' + end + end + + context 'merge pre-condition checks' do + before do + merge_request.project.update!(merge_method: merge_method) + end + + context 'when semi-linear merge method' do + let(:merge_method) { :rebase_merge } + + it 'return error when MR should be able to fast-forward' do + allow(merge_request).to receive(:should_be_rebased?) { true } + + error_message = 'Fast-forward merge is not possible. Please update your source branch.' + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when fast-forward merge method' do + let(:merge_method) { :ff } + + it 'returns error' do + error_message = "Fast-forward to #{merge_request.merge_ref_path} is currently not supported." + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when MR is not mergeable to ref' do + let(:merge_method) { :merge } + + it 'returns error' do + allow(merge_request).to receive(:mergeable_to_ref?) { false } + + error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}" + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + end + + context 'does not close related todos' do + let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:project) { merge_request.project } + let!(:todo) do + create(:todo, :assigned, + project: project, + author: user, + user: user, + target: merge_request) + end + + before do + allow(service).to receive(:execute_hooks) + + perform_enqueued_jobs do + service.execute(merge_request) + todo.reload + end + end + + it { expect(todo).not_to be_done } + end + + it 'returns error when user has no authorization to admin the merge request' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('You are not allowed to merge to this ref') + end + end +end diff --git a/spec/support/matchers/not_changed_matcher.rb b/spec/support/matchers/not_changed_matcher.rb new file mode 100644 index 00000000000..8ef4694982d --- /dev/null +++ b/spec/support/matchers/not_changed_matcher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/yarn.lock b/yarn.lock index 3143524e331..ea822b23113 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6371,6 +6371,11 @@ jest-snapshot@^23.6.0: pretty-format "^23.6.0" semver "^5.5.0" +jest-transform-graphql@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/jest-transform-graphql/-/jest-transform-graphql-2.1.0.tgz#903cb66bb27bc2772fd3e5dd4f7e9b57230f5829" + integrity sha1-kDy2a7J7wncv0+XdT36bVyMPWCk= + jest-util@^23.4.0: version "23.4.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561" |