diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 4221f097be3..a2dd79ed1ab 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -17,7 +17,7 @@ - [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). - [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html). - [ ] Link docs to and from the higher-level index page, plus other related docs where helpful. -- [ ] Apply the ~Documentation label. +- [ ] Apply the ~documentation label. ## Review checklist diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index f1cc6756583..a5e38022b8d 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -4,96 +4,97 @@ import Api from './api'; import { normalizeHeaders } from './lib/utils/common_utils'; import { __ } from '~/locale'; -export default function groupsSelect() { - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(() => { - // Needs to be accessible in rspec - window.GROUP_SELECT_PER_PAGE = 20; - $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { - const $select = $(this); - const allAvailable = $select.data('allAvailable'); - const skipGroups = $select.data('skipGroups') || []; - const parentGroupID = $select.data('parentId'); - const groupsPath = parentGroupID - ? Api.subgroupsPath.replace(':id', parentGroupID) - : Api.groupsPath; +const groupsSelect = () => { + // Needs to be accessible in rspec + window.GROUP_SELECT_PER_PAGE = 20; + $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { + const $select = $(this); + const allAvailable = $select.data('allAvailable'); + const skipGroups = $select.data('skipGroups') || []; + const parentGroupID = $select.data('parentId'); + const groupsPath = parentGroupID + ? Api.subgroupsPath.replace(':id', parentGroupID) + : Api.groupsPath; - $select.select2({ - placeholder: __('Search for a group'), - allowClear: $select.hasClass('allowClear'), - multiple: $select.hasClass('multiselect'), - minimumInputLength: 0, - ajax: { - url: Api.buildUrl(groupsPath), - dataType: 'json', - quietMillis: 250, - transport(params) { - axios[params.type.toLowerCase()](params.url, { - params: params.data, - }) - .then(res => { - const results = res.data || []; - const headers = normalizeHeaders(res.headers); - const currentPage = parseInt(headers['X-PAGE'], 10) || 0; - const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; - const more = currentPage < totalPages; + $select.select2({ + placeholder: __('Search for a group'), + allowClear: $select.hasClass('allowClear'), + multiple: $select.hasClass('multiselect'), + minimumInputLength: 0, + ajax: { + url: Api.buildUrl(groupsPath), + dataType: 'json', + quietMillis: 250, + transport(params) { + axios[params.type.toLowerCase()](params.url, { + params: params.data, + }) + .then(res => { + const results = res.data || []; + const headers = normalizeHeaders(res.headers); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; - params.success({ - results, - pagination: { - more, - }, - }); - }) - .catch(params.error); - }, - data(search, page) { - return { - search, - page, - per_page: window.GROUP_SELECT_PER_PAGE, - all_available: allAvailable, - }; - }, - results(data, page) { - if (data.length) return { results: [] }; - - const groups = data.length ? data : data.results || []; - const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); - - return { + params.success({ results, - page, - more, - }; - }, - }, - // eslint-disable-next-line consistent-return - initSelection(element, callback) { - const id = $(element).val(); - if (id !== '') { - return Api.group(id, callback); - } - }, - formatResult(object) { - return `
${object.full_name}
${object.full_path}
`; - }, - formatSelection(object) { - return object.full_name; - }, - dropdownCssClass: 'ajax-groups-dropdown select2-infinite', - // we do not want to escape markup since we are displaying html in results - escapeMarkup(m) { - return m; - }, - }); + pagination: { + more, + }, + }); + }) + .catch(params.error); + }, + data(search, page) { + return { + search, + page, + per_page: window.GROUP_SELECT_PER_PAGE, + all_available: allAvailable, + }; + }, + results(data, page) { + if (data.length) return { results: [] }; - $select.on('select2-loaded', () => { - const dropdown = document.querySelector('.select2-infinite .select2-results'); - dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; - }); - }); - }) + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); + + return { + results, + page, + more, + }; + }, + }, + // eslint-disable-next-line consistent-return + initSelection(element, callback) { + const id = $(element).val(); + if (id !== '') { + return Api.group(id, callback); + } + }, + formatResult(object) { + return `
${object.full_name}
${object.full_path}
`; + }, + formatSelection(object) { + return object.full_name; + }, + dropdownCssClass: 'ajax-groups-dropdown select2-infinite', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); + + $select.on('select2-loaded', () => { + const dropdown = document.querySelector('.select2-infinite .select2-results'); + dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; + }); + }); +}; + +export default () => + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(groupsSelect) .catch(() => {}); -} diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 88218c3c918..22b062563b5 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -120,7 +120,7 @@ export default class LabelsSelect { labelCount = 0; if (data.labels.length && issueUpdateURL) { template = LabelsSelect.getLabelTemplate({ - labels: data.labels, + labels: _.sortBy(data.labels, 'title'), issueUpdateURL, enableScopedLabels: scopedLabels, scopedLabelsDocumentationLink, diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 293d76ea765..c37e799de62 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Import::BitbucketController < Import::BaseController + include ActionView::Helpers::SanitizeHelper + before_action :verify_bitbucket_import_enabled before_action :bitbucket_auth, except: :callback @@ -21,7 +23,7 @@ class Import::BitbucketController < Import::BaseController # rubocop: disable CodeReuse/ActiveRecord def status bitbucket_client = Bitbucket::Client.new(credentials) - repos = bitbucket_client.repos + repos = bitbucket_client.repos(filter: sanitized_filter_param) @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } @@ -104,4 +106,8 @@ class Import::BitbucketController < Import::BaseController refresh_token: session[:bitbucket_refresh_token] } end + + def sanitized_filter_param + @filter ||= sanitize(params[:filter]) + end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 61bd50616b8..8cc31200689 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -9,6 +9,7 @@ module Groups def execute remove_unallowed_params + set_visibility_level @group = Group.new(params) @@ -68,6 +69,12 @@ module Groups true end + + def set_visibility_level + return if visibility_level.present? + + params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility + end end end diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 2336e1e83f9..7399ff937ce 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -8,7 +8,6 @@ - if @repos.any? %p.light = _('Select projects you want to import.') - %hr %p - if @incompatible_repos.any? = button_tag class: 'btn btn-import btn-success js-import-all' do @@ -19,6 +18,14 @@ = _('Import all projects') = icon('spinner spin', class: 'loading-icon') +.position-relative.ms-no-clear.d-flex.flex-fill.float-right.append-bottom-10 + = form_tag status_import_bitbucket_path, method: 'get' do + = text_field_tag :filter, @filter, class: 'form-control pr-5', placeholder: _('Filter projects'), size: 40, autofocus: true, 'aria-label': _('Search') + .position-absolute.position-top-0.d-flex.align-items-center.text-muted.position-right-0.h-100 + .border-left + %button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' } + %i{ class: 'fa fa-search', 'aria-hidden': true } + .table-responsive %table.table.import-jobs %colgroup.import-jobs-from-col @@ -59,7 +66,7 @@ - if current_user.can_select_namespace? - selected = params[:namespace_id] || :current_user - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 } + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - else = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true %span.input-group-prepend diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index aac09801d91..1aaf5883bf4 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -62,7 +62,7 @@ - if current_user.can_select_namespace? - selected = params[:namespace_id] || :extra_group - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 } + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - else = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true %span.input-group-prepend diff --git a/changelogs/unreleased/31393-when-adding-labels-in-a-merge-request-adds-them-out-of-order-until-.yml b/changelogs/unreleased/31393-when-adding-labels-in-a-merge-request-adds-them-out-of-order-until-.yml new file mode 100644 index 00000000000..2b47255a06f --- /dev/null +++ b/changelogs/unreleased/31393-when-adding-labels-in-a-merge-request-adds-them-out-of-order-until-.yml @@ -0,0 +1,5 @@ +--- +title: Alphabetically sorts selected sidebar labels. +merge_request: 17309 +author: +type: fixed diff --git a/changelogs/unreleased/ancestor_groups.yml b/changelogs/unreleased/ancestor_groups.yml new file mode 100644 index 00000000000..1b5d6573b86 --- /dev/null +++ b/changelogs/unreleased/ancestor_groups.yml @@ -0,0 +1,5 @@ +--- +title: 'Allow to exclude ancestor groups on group labels API' +merge_request: 17221 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/georgekoltsov-bitbucket-cloud-import-filtering.yml b/changelogs/unreleased/georgekoltsov-bitbucket-cloud-import-filtering.yml new file mode 100644 index 00000000000..8fdbdb20d28 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-bitbucket-cloud-import-filtering.yml @@ -0,0 +1,5 @@ +--- +title: Add project filtering to Bitbucket Cloud import +merge_request: 16828 +author: +type: added diff --git a/changelogs/unreleased/group_default_visibility.yml b/changelogs/unreleased/group_default_visibility.yml new file mode 100644 index 00000000000..3816223c066 --- /dev/null +++ b/changelogs/unreleased/group_default_visibility.yml @@ -0,0 +1,5 @@ +--- +title: Fix visibility level error when updating group from API +merge_request: 17227 +author: Mathieu Parent +type: fixed diff --git a/changelogs/unreleased/update-schema-rb.yml b/changelogs/unreleased/update-schema-rb.yml new file mode 100644 index 00000000000..7e0756dc489 --- /dev/null +++ b/changelogs/unreleased/update-schema-rb.yml @@ -0,0 +1,5 @@ +--- +title: Remove Postgresql specific setup tasks and move to schema.rb +merge_request: +author: +type: other diff --git a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb index 4c3bb0a26ce..2b9365ce827 100644 --- a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb +++ b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb @@ -20,9 +20,7 @@ class ReworkRedirectRoutesIndexes < ActiveRecord::Migration[4.2] def up disable_statement_timeout do # this is a plain btree on a single boolean column. It'll never be - # selective enough to be valuable. This class is called by - # setup_postgresql.rake so it needs to be able to handle this - # index not existing. + # selective enough to be valuable. if index_exists?(:redirect_routes, :permanent) remove_concurrent_index(:redirect_routes, :permanent) end diff --git a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb index 53918250b4c..d44ec1036c4 100644 --- a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb +++ b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb @@ -14,9 +14,7 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration[4.2] # RedirectRoute.matching_path_and_descendants # # This same index is also added in the `ReworkRedirectRoutesIndexes` so this - # is a no-op in most cases. But this migration is also called from the - # `setup_postgresql.rake` task when setting up a new database, in which case - # we want to create the index. + # is a no-op in most cases. def up return unless Gitlab::Database.postgresql? @@ -31,8 +29,5 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration[4.2] # Do nothing in the DOWN. Since the index above is originally created in the # `ReworkRedirectRoutesIndexes`. This migration wouldn't have actually # created any new index. - # - # This migration is only here to be called form `setup_postgresql.rake` so - # any newly created database would have this index. end end diff --git a/db/schema.rb b/db/schema.rb index aff672919c1..8fcced21d56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2917,6 +2917,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do t.boolean "emails_disabled" t.integer "max_pages_size" t.integer "max_artifacts_size" + t.index "lower((name)::text)", name: "index_projects_on_lower_name" t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))" t.index ["created_at"], name: "index_projects_on_created_at" t.index ["creator_id"], name: "index_projects_on_creator_id" @@ -3113,6 +3114,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do t.string "path", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index "lower((path)::text) varchar_pattern_ops", name: "index_redirect_routes_on_path_unique_text_pattern_ops", unique: true t.index ["path"], name: "index_redirect_routes_on_path", unique: true t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id" end @@ -3641,6 +3643,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do t.string "first_name", limit: 255 t.string "last_name", limit: 255 t.string "static_object_token", limit: 255 + t.index "lower((name)::text)", name: "index_on_users_name_lower" t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id" t.index ["admin"], name: "index_users_on_admin" t.index ["bot_type"], name: "index_users_on_bot_type" diff --git a/doc/api/group_labels.md b/doc/api/group_labels.md index 5030bba3159..9563f967a2a 100644 --- a/doc/api/group_labels.md +++ b/doc/api/group_labels.md @@ -16,6 +16,7 @@ GET /groups/:id/labels | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. | | `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31543))_ | +| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/5/labels?with_counts=true diff --git a/doc/api/labels.md b/doc/api/labels.md index 93833fd81cb..f29c0a28cdf 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -12,6 +12,7 @@ GET /projects/:id/labels | --------- | ------- | -------- | --------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31543))_ | +| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/projects/1/labels?with_counts=true diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md index e509e333313..77fc2761e07 100644 --- a/doc/user/project/import/bitbucket.md +++ b/doc/user/project/import/bitbucket.md @@ -56,10 +56,10 @@ namespace that started the import process. ![Grant access](img/bitbucket_import_grant_access.png) 1. Click on the projects that you'd like to import or **Import all projects**. - You can also select the namespace under which each project will be - imported. + You can also filter projects by name and select the namespace under which + each project will be imported. - ![Import projects](img/bitbucket_import_select_project.png) + ![Import projects](img/bitbucket_import_select_project_v12_3.png) [bb-import]: ../../../integration/bitbucket.md [social sign-in]: ../../profile/account/social_sign_in.md diff --git a/doc/user/project/import/img/bitbucket_import_select_project.png b/doc/user/project/import/img/bitbucket_import_select_project.png deleted file mode 100644 index 1bca6166ec8..00000000000 Binary files a/doc/user/project/import/img/bitbucket_import_select_project.png and /dev/null differ diff --git a/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png b/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png new file mode 100644 index 00000000000..1f1febd9068 Binary files /dev/null and b/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png differ diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb index 79a44941c81..cb044d4095f 100644 --- a/lib/api/group_labels.rb +++ b/lib/api/group_labels.rb @@ -18,10 +18,12 @@ module API params do optional :with_counts, type: Boolean, default: false, desc: 'Include issue and merge request counts' + optional :include_ancestor_groups, type: Boolean, default: true, + desc: 'Include ancestor groups' use :pagination end get ':id/labels' do - get_labels(user_group, Entities::GroupLabel) + get_labels(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups]) end desc 'Create a new label' do diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index 585ae1eb5c4..abe9d457a5b 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -10,8 +10,6 @@ module API optional :description, type: String, desc: 'The description of the group' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, - default: Gitlab::VisibilityLevel.string_level( - Gitlab::CurrentSettings.current_application_settings.default_group_visibility), desc: 'The visibility of the group' optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' diff --git a/lib/api/helpers/label_helpers.rb b/lib/api/helpers/label_helpers.rb index ec5b688dd1c..126747e4f34 100644 --- a/lib/api/helpers/label_helpers.rb +++ b/lib/api/helpers/label_helpers.rb @@ -18,8 +18,8 @@ module API label || not_found!('Label') end - def get_labels(parent, entity) - present paginate(available_labels_for(parent)), + def get_labels(parent, entity, include_ancestor_groups: true) + present paginate(available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)), with: entity, current_user: current_user, parent: parent, diff --git a/lib/api/labels.rb b/lib/api/labels.rb index de89e94b0c0..12553cbbbfa 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -17,10 +17,12 @@ module API params do optional :with_counts, type: Boolean, default: false, desc: 'Include issue and merge request counts' + optional :include_ancestor_groups, type: Boolean, default: true, + desc: 'Include ancestor groups' use :pagination end get ':id/labels' do - get_labels(user_project, Entities::ProjectLabel) + get_labels(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups]) end desc 'Create a new label' do diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb index 1343f424c51..92894575ec2 100644 --- a/lib/bitbucket/client.rb +++ b/lib/bitbucket/client.rb @@ -38,8 +38,10 @@ module Bitbucket Representation::Repo.new(parsed_response) end - def repos + def repos(filter: nil) path = "/repositories?role=member" + path += "&q=name~\"#{filter}\"" if filter + get_collection(path, :repo) end diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 5d86d6e466c..50774de77c9 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -31,7 +31,6 @@ namespace :gitlab do terminate_all_connections unless Rails.env.production? Rake::Task["db:reset"].invoke - Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError puts "Quitting...".color(:red) diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index cda88c130bb..4c8f13b63a4 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -1,14 +1,3 @@ -desc 'GitLab | Sets up PostgreSQL' -task setup_postgresql: :environment do - require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb') - require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb') - require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb') - - UsersNameLowerIndex.new.up - ProjectNameLowerIndex.new.up - AddPathIndexToRedirectRoutes.new.up -end - desc 'GitLab | Generate PostgreSQL Password Hash' task :postgresql_md5_hash do require 'digest' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c8b44197514..e66140789a2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6795,6 +6795,9 @@ msgstr "" msgid "Filter by two-factor authentication" msgstr "" +msgid "Filter projects" +msgstr "" + msgid "Filter results by group" msgstr "" @@ -13602,6 +13605,9 @@ msgstr "" msgid "Search" msgstr "" +msgid "Search Button" +msgstr "" + msgid "Search an environment spec" msgstr "" diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 5853f487f0b..751bdb32506 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -26,8 +26,16 @@ module QA::Page end # Reminder: You may wish to wait for a particular job status before checking output - def output - find_element(:build_trace).text + def output(wait: 5) + result = '' + + wait(reload: false, max: wait, interval: 1) do + result = find_element(:build_trace).text + + !result.empty? + end + + result end private diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 38388c21749..d013bd6d427 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -80,6 +80,21 @@ describe Import::BitbucketController do expect(assigns(:already_added_projects)).to eq([@project]) expect(assigns(:repos)).to eq([]) end + + context 'when filtering' do + let(:filter) { 'test' } + let(:expected_filter) { 'test' } + + subject { get :status, params: { filter: filter }, as: :json } + + it 'passes sanitized filter param to bitbucket client' do + expect_next_instance_of(Bitbucket::Client) do |client| + expect(client).to receive(:repos).with(filter: expected_filter).and_return([@repo]) + end + + subject + end + end end describe "POST create" do diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js index ccf439aac74..5ae5643aefc 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/javascripts/labels_issue_sidebar_spec.js @@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import IssuableContext from '~/issuable_context'; import LabelsSelect from '~/labels_select'; +import _ from 'underscore'; import '~/gl_dropdown'; import 'select2'; @@ -15,6 +16,35 @@ import '~/users_select'; let saveLabelCount = 0; let mock; +function testLabelClicks(labelOrder, done) { + $('.edit-link') + .get(0) + .click(); + + setTimeout(() => { + const labelsInDropdown = $('.dropdown-content a'); + + expect(labelsInDropdown.length).toBe(10); + + const arrayOfLabels = labelsInDropdown.get(); + const randomArrayOfLabels = _.shuffle(arrayOfLabels); + randomArrayOfLabels.forEach((label, i) => { + if (i < saveLabelCount) { + $(label).click(); + } + }); + + $('.edit-link') + .get(0) + .click(); + + setTimeout(() => { + expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(labelOrder); + done(); + }, 0); + }, 0); +} + describe('Issue dropdown sidebar', () => { preloadFixtures('static/issue_sidebar_label.html'); @@ -29,7 +59,7 @@ describe('Issue dropdown sidebar', () => { mock.onGet('/root/test/labels.json').reply(() => { const labels = Array(10) .fill() - .map((_, i) => ({ + .map((_val, i) => ({ id: i, title: `test ${i}`, color: '#5CB85C', @@ -41,7 +71,7 @@ describe('Issue dropdown sidebar', () => { mock.onPut('/root/test/issues/2.json').reply(() => { const labels = Array(saveLabelCount) .fill() - .map((_, i) => ({ + .map((_val, i) => ({ id: i, title: `test ${i}`, color: '#5CB85C', @@ -57,61 +87,11 @@ describe('Issue dropdown sidebar', () => { it('changes collapsed tooltip when changing labels when less than 5', done => { saveLabelCount = 5; - $('.edit-link') - .get(0) - .click(); - - setTimeout(() => { - expect($('.dropdown-content a').length).toBe(10); - - $('.dropdown-content a').each(function(i) { - if (i < saveLabelCount) { - $(this) - .get(0) - .click(); - } - }); - - $('.edit-link') - .get(0) - .click(); - - setTimeout(() => { - expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe( - 'test 0, test 1, test 2, test 3, test 4', - ); - done(); - }, 0); - }, 0); + testLabelClicks('test 0, test 1, test 2, test 3, test 4', done); }); it('changes collapsed tooltip when changing labels when more than 5', done => { saveLabelCount = 6; - $('.edit-link') - .get(0) - .click(); - - setTimeout(() => { - expect($('.dropdown-content a').length).toBe(10); - - $('.dropdown-content a').each(function(i) { - if (i < saveLabelCount) { - $(this) - .get(0) - .click(); - } - }); - - $('.edit-link') - .get(0) - .click(); - - setTimeout(() => { - expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe( - 'test 0, test 1, test 2, test 3, test 4, and 1 more', - ); - done(); - }, 0); - }, 0); + testLabelClicks('test 0, test 1, test 2, test 3, test 4, and 1 more', done); }); }); diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb index 0be4e2e9121..3ac394b57c5 100644 --- a/spec/requests/api/group_labels_spec.rb +++ b/spec/requests/api/group_labels_spec.rb @@ -5,9 +5,11 @@ require 'spec_helper' describe API::GroupLabels do let(:user) { create(:user) } let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } let!(:group_member) { create(:group_member, group: group, user: user) } - let!(:label1) { create(:group_label, title: 'feature', group: group) } - let!(:label2) { create(:group_label, title: 'bug', group: group) } + let!(:group_label1) { create(:group_label, title: 'feature', group: group) } + let!(:group_label2) { create(:group_label, title: 'bug', group: group) } + let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } describe 'GET :id/labels' do it 'returns all available labels for the group' do @@ -35,6 +37,34 @@ describe API::GroupLabels do end end + describe 'GET :subgroup_id/labels' do + context 'when the include_ancestor_groups parameter is not set' do + it 'returns all available labels for the group and ancestor groups' do + get api("/groups/#{subgroup.id}/labels", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(3) + expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support') + end + end + + context 'when the include_ancestor_groups parameter is set to false' do + it 'returns all available labels for the group but not for ancestor groups' do + get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false } + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(1) + expect(json_response.map {|r| r['name'] }).to contain_exactly('support') + end + end + end + describe 'POST /groups/:id/labels' do it 'returns created label when all params are given' do post api("/groups/#{group.id}/labels", user), @@ -78,7 +108,7 @@ describe API::GroupLabels do it 'returns 409 if label already exists' do post api("/groups/#{group.id}/labels", user), params: { - name: label1.name, + name: group_label1.name, color: '#FFAABB' } @@ -89,13 +119,13 @@ describe API::GroupLabels do describe 'DELETE /groups/:id/labels' do it 'returns 204 for existing label' do - delete api("/groups/#{group.id}/labels", user), params: { name: label1.name } + delete api("/groups/#{group.id}/labels", user), params: { name: group_label1.name } expect(response).to have_gitlab_http_status(204) end it 'returns 404 for non existing label' do - delete api("/groups/#{group.id}/labels", user), params: { name: 'label2' } + delete api("/groups/#{group.id}/labels", user), params: { name: 'not_exists' } expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 Label Not Found') @@ -115,12 +145,12 @@ describe API::GroupLabels do expect(response).to have_gitlab_http_status(204) expect(subgroup.labels.size).to eq(0) - expect(group.labels).to include(label1) + expect(group.labels).to include(group_label1) end it_behaves_like '412 response' do let(:request) { api("/groups/#{group.id}/labels", user) } - let(:params) { { name: label1.name } } + let(:params) { { name: group_label1.name } } end end @@ -128,7 +158,7 @@ describe API::GroupLabels do it 'returns 200 if name and colors and description are changed' do put api("/groups/#{group.id}/labels", user), params: { - name: label1.name, + name: group_label1.name, new_name: 'New Label', color: '#FFFFFF', description: 'test' @@ -152,13 +182,13 @@ describe API::GroupLabels do expect(response).to have_gitlab_http_status(200) expect(subgroup.labels[0].name).to eq('New Label') - expect(label1.name).to eq('feature') + expect(group_label1.name).to eq('feature') end it 'returns 404 if label does not exist' do put api("/groups/#{group.id}/labels", user), params: { - name: 'label2', + name: 'not_exists', new_name: 'label3' } @@ -166,14 +196,14 @@ describe API::GroupLabels do end it 'returns 400 if no label name given' do - put api("/groups/#{group.id}/labels", user), params: { new_name: label1.name } + put api("/groups/#{group.id}/labels", user), params: { new_name: group_label1.name } expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq('name is missing') end it 'returns 400 if no new parameters given' do - put api("/groups/#{group.id}/labels", user), params: { name: label1.name } + put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name } expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq('new_name, color, description are missing, '\ @@ -184,31 +214,31 @@ describe API::GroupLabels do describe 'POST /groups/:id/labels/:label_id/subscribe' do context 'when label_id is a label title' do it 'subscribes to the label' do - post api("/groups/#{group.id}/labels/#{label1.title}/subscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.title}/subscribe", user) expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq(label1.title) + expect(json_response['name']).to eq(group_label1.title) expect(json_response['subscribed']).to be_truthy end end context 'when label_id is a label ID' do it 'subscribes to the label' do - post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user) expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq(label1.title) + expect(json_response['name']).to eq(group_label1.title) expect(json_response['subscribed']).to be_truthy end end context 'when user is already subscribed to label' do before do - label1.subscribe(user) + group_label1.subscribe(user) end it 'returns 304' do - post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.id}/subscribe", user) expect(response).to have_gitlab_http_status(304) end @@ -225,36 +255,36 @@ describe API::GroupLabels do describe 'POST /groups/:id/labels/:label_id/unsubscribe' do before do - label1.subscribe(user) + group_label1.subscribe(user) end context 'when label_id is a label title' do it 'unsubscribes from the label' do - post api("/groups/#{group.id}/labels/#{label1.title}/unsubscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.title}/unsubscribe", user) expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq(label1.title) + expect(json_response['name']).to eq(group_label1.title) expect(json_response['subscribed']).to be_falsey end end context 'when label_id is a label ID' do it 'unsubscribes from the label' do - post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user) expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq(label1.title) + expect(json_response['name']).to eq(group_label1.title) expect(json_response['subscribed']).to be_falsey end end context 'when user is already unsubscribed from label' do before do - label1.unsubscribe(user) + group_label1.unsubscribe(user) end it 'returns 304' do - post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user) + post api("/groups/#{group.id}/labels/#{group_label1.id}/unsubscribe", user) expect(response).to have_gitlab_http_status(304) end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index b3acf531ccb..ec587a28f4f 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -497,6 +497,29 @@ describe API::Groups do expect(response).to have_gitlab_http_status(404) end + + context 'within a subgroup' do + let(:group3) { create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let!(:subgroup) { create(:group, parent: group3, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + + before do + group3.add_owner(user3) + end + + it 'does not change visibility when not requested' do + put api("/groups/#{group3.id}", user3), params: { description: 'Bug #23083' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['visibility']).to eq('public') + end + + it 'prevents making private a group containing public subgroups' do + put api("/groups/#{group3.id}", user3), params: { visibility: 'private' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['visibility_level']).to contain_exactly('private is not allowed since there are sub-groups with higher visibility.') + end + end end context 'when authenticated as the admin' do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 9aef67e28a7..4f8233a9110 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -256,6 +256,52 @@ describe API::Labels do 'is_project_label' => true) end end + + context 'when the include_ancestor_groups parameter is not set' do + let(:group) { create(:group) } + let!(:group_label) { create(:group_label, title: 'feature', group: group) } + let(:subgroup) { create(:group, parent: group) } + let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } + + before do + subgroup.add_owner(user) + project.update!(group: subgroup) + end + + it 'returns all available labels for the project, parent group and ancestor groups' do + get api("/projects/#{project.id}/labels", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(4) + expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name) + end + end + + context 'when the include_ancestor_groups parameter is set to false' do + let(:group) { create(:group) } + let!(:group_label) { create(:group_label, title: 'feature', group: group) } + let(:subgroup) { create(:group, parent: group) } + let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } + + before do + subgroup.add_owner(user) + project.update!(group: subgroup) + end + + it 'returns all available labels for the project and the parent group only' do + get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false } + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(3) + expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name) + end + end end describe 'POST /projects/:id/labels' do