Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/merge_request_templates/Documentation.md2
-rw-r--r--app/assets/javascripts/groups_select.js175
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/controllers/import/bitbucket_controller.rb8
-rw-r--r--app/services/groups/create_service.rb7
-rw-r--r--app/views/import/bitbucket/status.html.haml11
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--changelogs/unreleased/31393-when-adding-labels-in-a-merge-request-adds-them-out-of-order-until-.yml5
-rw-r--r--changelogs/unreleased/ancestor_groups.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-bitbucket-cloud-import-filtering.yml5
-rw-r--r--changelogs/unreleased/group_default_visibility.yml5
-rw-r--r--changelogs/unreleased/update-schema-rb.yml5
-rw-r--r--db/migrate/20180113220114_rework_redirect_routes_indexes.rb4
-rw-r--r--db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb7
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/group_labels.md1
-rw-r--r--doc/api/labels.md1
-rw-r--r--doc/user/project/import/bitbucket.md6
-rw-r--r--doc/user/project/import/img/bitbucket_import_select_project.pngbin8688 -> 0 bytes
-rw-r--r--doc/user/project/import/img/bitbucket_import_select_project_v12_3.pngbin0 -> 112397 bytes
-rw-r--r--lib/api/group_labels.rb4
-rw-r--r--lib/api/helpers/groups_helpers.rb2
-rw-r--r--lib/api/helpers/label_helpers.rb4
-rw-r--r--lib/api/labels.rb4
-rw-r--r--lib/bitbucket/client.rb4
-rw-r--r--lib/tasks/gitlab/setup.rake1
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake11
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/job/show.rb12
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb15
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js88
-rw-r--r--spec/requests/api/group_labels_spec.rb80
-rw-r--r--spec/requests/api/groups_spec.rb23
-rw-r--r--spec/requests/api/labels_spec.rb46
34 files changed, 349 insertions, 205 deletions
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: [] };
+ 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);
+ 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 `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- 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;
- },
- });
+ 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 `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ 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`;
- });
- });
- })
+ $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: <your_access_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: <your_access_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
--- a/doc/user/project/import/img/bitbucket_import_select_project.png
+++ /dev/null
Binary files 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
--- /dev/null
+++ b/doc/user/project/import/img/bitbucket_import_select_project_v12_3.png
Binary files 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) { '<html>test</html>' }
+ 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