Commit d1f9b8d1 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-08-01

# Conflicts:
#	app/models/resource_label_event.rb
#	app/services/projects/create_from_template_service.rb
#	app/services/projects/gitlab_projects_import_service.rb
#	app/services/resource_events/change_labels_service.rb
#	db/migrate/20180726172057_create_resource_label_events.rb
#	db/schema.rb
#	doc/administration/index.md
#	doc/api/issues.md
#	locale/gitlab.pot

[ci skip]
parents 07cef0e5 6cccf59c
...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController
def find_labels def find_labels
@available_labels ||= @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute LabelsFinder.new(current_user,
project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups],
search: params[:search]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder ...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none items = find_union(label_ids, Label) || Label.none
items = with_title(items) items = with_title(items)
items = by_search(items)
sort(items) sort(items)
end end
...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder ...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def by_search(labels)
return labels unless search?
labels.search(params[:search])
end
# Gets redacted array of group ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group. # which can include the ancestors and descendants of the requested group.
def group_ids_for(group) def group_ids_for(group)
...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder ...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder
params[:only_group_labels] params[:only_group_labels]
end end
def search?
params[:search].present?
end
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
......
...@@ -35,16 +35,21 @@ module AtomicInternalId ...@@ -35,16 +35,21 @@ module AtomicInternalId
define_method("ensure_#{scope}_#{column}!") do define_method("ensure_#{scope}_#{column}!") do
scope_value = association(scope).reader scope_value = association(scope).reader
value = read_attribute(column)
if read_attribute(column).blank? && scope_value return value unless scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init) scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
write_attribute(column, new_iid) usage = self.class.table_name.to_sym
if value.present?
InternalId.track_greatest(self, scope_attrs, usage, value, init)
else
value = InternalId.generate_next(self, scope_attrs, usage, init)
write_attribute(column, value)
end end
read_attribute(column) value
end end
end end
end end
......
# An InternalId is a strictly monotone sequence of integers # An InternalId is a strictly monotone sequence of integers
# generated for a given scope and usage. # generated for a given scope and usage.
# #
# The monotone sequence may be broken if an ID is explicitly provided
# to `.track_greatest_and_save!` or `#track_greatest`.
#
# For example, issues use their project to scope internal ids: # For example, issues use their project to scope internal ids:
# In that sense, scope is "project" and usage is "issues". # In that sense, scope is "project" and usage is "issues".
# Generated internal ids for an issue are unique per project. # Generated internal ids for an issue are unique per project.
...@@ -25,13 +28,34 @@ class InternalId < ActiveRecord::Base ...@@ -25,13 +28,34 @@ class InternalId < ActiveRecord::Base
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently. # As such, the increment is atomic and safe to be called concurrently.
def increment_and_save! def increment_and_save!
update_and_save { self.last_value = (last_value || 0) + 1 }
end
# Increments #last_value with new_value if it is greater than the current,
# and saves the record
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
def track_greatest_and_save!(new_value)
update_and_save { self.last_value = [last_value || 0, new_value].max }
end
private
def update_and_save(&block)
lock! lock!
self.last_value = (last_value || 0) + 1 yield
save! save!
last_value last_value
end end
class << self class << self
def track_greatest(subject, scope, usage, new_value, init)
return new_value unless available?
InternalIdGenerator.new(subject, scope, usage, init).track_greatest(new_value)
end
def generate_next(subject, scope, usage, init) def generate_next(subject, scope, usage, init)
# Shortcut if `internal_ids` table is not available (yet) # Shortcut if `internal_ids` table is not available (yet)
# This can be the case in other (unrelated) migration specs # This can be the case in other (unrelated) migration specs
...@@ -94,6 +118,16 @@ class InternalId < ActiveRecord::Base ...@@ -94,6 +118,16 @@ class InternalId < ActiveRecord::Base
end end
end end
# Create a record in internal_ids if one does not yet exist
# and set its new_value if it is higher than the current last_value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
subject.transaction do
(lookup || create_record).track_greatest_and_save!(new_value)
end
end
private private
# Retrieve InternalId record for (project, usage) combination, if it exists # Retrieve InternalId record for (project, usage) combination, if it exists
......
...@@ -2,6 +2,7 @@ class Label < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class Label < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include Referable include Referable
include Subscribable include Subscribable
include Gitlab::SQL::Pattern
# Represents a "No Label" state used for filtering Issues and Merge # Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned. # Requests that have no label assigned.
...@@ -103,6 +104,17 @@ class Label < ActiveRecord::Base ...@@ -103,6 +104,17 @@ class Label < ActiveRecord::Base
nil nil
end end
# Searches for labels with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def self.search(query)
fuzzy_search(query, [:title, :description])
end
def open_issues_count(user = nil) def open_issues_count(user = nil)
issues_count(user, state: 'opened') issues_count(user, state: 'opened')
end end
......
...@@ -3,8 +3,11 @@ ...@@ -3,8 +3,11 @@
# This model is not used yet, it will be used for: # This model is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 # https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
class ResourceLabelEvent < ActiveRecord::Base class ResourceLabelEvent < ActiveRecord::Base
<<<<<<< HEAD
prepend EE::ResourceLabelEvent prepend EE::ResourceLabelEvent
=======
>>>>>>> upstream/master
belongs_to :user belongs_to :user
belongs_to :issue belongs_to :issue
belongs_to :merge_request belongs_to :merge_request
......
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
module Projects module Projects
class CreateFromTemplateService < BaseService class CreateFromTemplateService < BaseService
<<<<<<< HEAD
prepend ::EE::Projects::CreateFromTemplateService prepend ::EE::Projects::CreateFromTemplateService
=======
>>>>>>> upstream/master
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(user, params) def initialize(user, params)
......
...@@ -5,7 +5,10 @@ ...@@ -5,7 +5,10 @@
# The latter will under the hood just import an archive supplied by GitLab. # The latter will under the hood just import an archive supplied by GitLab.
module Projects module Projects
class GitlabProjectsImportService class GitlabProjectsImportService
<<<<<<< HEAD
prepend ::EE::Projects::GitlabProjectsImportService prepend ::EE::Projects::GitlabProjectsImportService
=======
>>>>>>> upstream/master
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include Gitlab::TemplateHelper include Gitlab::TemplateHelper
......
...@@ -4,8 +4,11 @@ ...@@ -4,8 +4,11 @@
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 # https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
module ResourceEvents module ResourceEvents
class ChangeLabelsService class ChangeLabelsService
<<<<<<< HEAD
prepend EE::ResourceEvents::ChangeLabelsService prepend EE::ResourceEvents::ChangeLabelsService
=======
>>>>>>> upstream/master
attr_reader :resource, :user attr_reader :resource, :user
def initialize(resource, user) def initialize(resource, user)
......
...@@ -2,32 +2,45 @@ ...@@ -2,32 +2,45 @@
- page_title "Labels" - page_title "Labels"
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- hide_class = '' - hide_class = ''
- search = params[:search]
- if can_admin_label - if can_admin_label
- content_for(:header_content) do - content_for(:header_content) do
.nav-controls .nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists? || search.present?
#promote-label-modal #promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.nav-text .nav-text
= _('Labels can be applied to issues and merge requests.') = _('Labels can be applied to issues and merge requests.')
- if can_admin_label
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
.labels-container.prepend-top-5 .nav-controls
= form_tag project_labels_path(@project), method: :get do
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false }
%span.input-group-append
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
.labels-container.prepend-top-10
- if can_admin_label - if can_admin_label
- if search.blank?
%p.text-muted
= _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
-# Only show it in the first page -# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) } .prioritized-labels{ class: ('hide' if hide) }
%h5.prepend-top-10= _('Prioritized Labels') %h5.prepend-top-10= _('Prioritized Labels')
.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels' = render 'shared/empty_states/priority_labels'
- if @prioritized_labels.present? - if @prioritized_labels.present?
= render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- elsif search.present?
.nothing-here-block
= _('No prioritised labels with such name or description')
- if @labels.present? - if @labels.present?
.other-labels .other-labels
...@@ -36,6 +49,18 @@ ...@@ -36,6 +49,18 @@
.content-list.manage-labels-list.js-other-labels .content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab' = paginate @labels, theme: 'gitlab'
- elsif search.present?
.other-labels
- if @available_labels.any?
%h5
= _('Other Labels')
.nothing-here-block
= _('No other labels with such name or description')
- else
.nothing-here-block
= _('No labels with such name or description')
- else - else
= render 'shared/empty_states/labels' = render 'shared/empty_states/labels'
......
---
title: Allow issues API to receive an internal ID (iid) on create
merge_request: 20626
author: Jamie Schembri
type: fixed
---
title: Search for labels by title or description on project labels page
merge_request: 20749
author:
type: added
---
title: Show one digit after dot in commit_per_day value in charts page.
merge_request:
author: msdundar
type: changed
...@@ -10,7 +10,10 @@ class CreateResourceLabelEvents < ActiveRecord::Migration ...@@ -10,7 +10,10 @@ class CreateResourceLabelEvents < ActiveRecord::Migration
t.integer :action, null: false t.integer :action, null: false
t.references :issue, null: true, index: true, foreign_key: { on_delete: :cascade } t.references :issue, null: true, index: true, foreign_key: { on_delete: :cascade }
t.references :merge_request, null: true, index: true, foreign_key: { on_delete: :cascade } t.references :merge_request, null: true, index: true, foreign_key: { on_delete: :cascade }
<<<<<<< HEAD
t.references :epic, null: true, index: true, foreign_key: { on_delete: :cascade } t.references :epic, null: true, index: true, foreign_key: { on_delete: :cascade }
=======
>>>>>>> upstream/master
t.references :label, index: true, foreign_key: { on_delete: :nullify } t.references :label, index: true, foreign_key: { on_delete: :nullify }
t.references :user, index: true, foreign_key: { on_delete: :nullify } t.references :user, index: true, foreign_key: { on_delete: :nullify }
t.datetime_with_timezone :created_at, null: false t.datetime_with_timezone :created_at, null: false
......
...@@ -2363,13 +2363,19 @@ ActiveRecord::Schema.define(version: 20180726172057) do ...@@ -2363,13 +2363,19 @@ ActiveRecord::Schema.define(version: 20180726172057) do
t.integer "action", null: false t.integer "action", null: false
t.integer "issue_id" t.integer "issue_id"
t.integer "merge_request_id" t.integer "merge_request_id"
<<<<<<< HEAD
t.integer "epic_id" t.integer "epic_id"
=======
>>>>>>> upstream/master
t.integer "label_id" t.integer "label_id"
t.integer "user_id" t.integer "user_id"
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
end end
<<<<<<< HEAD
add_index "resource_label_events", ["epic_id"], name: "index_resource_label_events_on_epic_id", using: :btree add_index "resource_label_events", ["epic_id"], name: "index_resource_label_events_on_epic_id", using: :btree
=======
>>>>>>> upstream/master
add_index "resource_label_events", ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree add_index "resource_label_events", ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree
add_index "resource_label_events", ["label_id"], name: "index_resource_label_events_on_label_id", using: :btree add_index "resource_label_events", ["label_id"], name: "index_resource_label_events_on_label_id", using: :btree
add_index "resource_label_events", ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id", using: :btree add_index "resource_label_events", ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id", using: :btree
...@@ -3049,13 +3055,20 @@ ActiveRecord::Schema.define(version: 20180726172057) do ...@@ -3049,13 +3055,20 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
<<<<<<< HEAD
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "resource_label_events", "epics", on_delete: :cascade add_foreign_key "resource_label_events", "epics", on_delete: :cascade
=======
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
>>>>>>> upstream/master
add_foreign_key "resource_label_events", "issues", on_delete: :cascade add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify add_foreign_key "resource_label_events", "labels", on_delete: :nullify
add_foreign_key "resource_label_events", "merge_requests", on_delete: :cascade add_foreign_key "resource_label_events", "merge_requests", on_delete: :cascade
add_foreign_key "resource_label_events", "users", on_delete: :nullify add_foreign_key "resource_label_events", "users", on_delete: :nullify
<<<<<<< HEAD
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
=======
>>>>>>> upstream/master
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
......
...@@ -124,7 +124,11 @@ created in snippets, wikis, and repos. ...@@ -124,7 +124,11 @@ created in snippets, wikis, and repos.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. - [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
<<<<<<< HEAD
- [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]** - [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
=======
- [Custom project templates](https://docs.gitlab.com/ee/user/admin_area/custom_project_templates.html): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
>>>>>>> upstream/master
### Repository settings ### Repository settings
......
...@@ -467,6 +467,7 @@ POST /projects/:id/issues ...@@ -467,6 +467,7 @@ POST /projects/:id/issues
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|-------------------------------------------|----------------|----------|--------------| |-------------------------------------------|----------------|----------|--------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
<<<<<<< HEAD
| `title` | string | yes | The title of an issue | | `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue | | `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
...@@ -478,6 +479,19 @@ POST /projects/:id/issues ...@@ -478,6 +479,19 @@ POST /projects/:id/issues
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.| | `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. | | `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
| `weight` | integer | no | The weight of the issue in range 0 to 9 | | `weight` | integer | no | The weight of the issue in range 0 to 9 |
=======
| `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) |
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue |
| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
>>>>>>> upstream/master
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
......
...@@ -167,6 +167,9 @@ module API ...@@ -167,6 +167,9 @@ module API
desc: 'The IID of a merge request for which to resolve discussions' desc: 'The IID of a merge request for which to resolve discussions'
optional :discussion_to_resolve, type: String, optional :discussion_to_resolve, type: String,
desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`' desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
optional :iid, type: Integer,
desc: 'The internal ID of a project issue. Available only for admins and project owners.'
use :issue_params use :issue_params
end end
post ':id/issues' do post ':id/issues' do
...@@ -174,9 +177,10 @@ module API ...@@ -174,9 +177,10 @@ module API
authorize! :create_issue, user_project authorize! :create_issue, user_project
# Setting created_at time only allowed for admins and project owners # Setting created_at time or iid only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at) params.delete(:created_at)
params.delete(:iid)
end end
issue_params = declared_params(include_missing: false) issue_params = declared_params(include_missing: false)
......
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
end end
def commit_per_day def commit_per_day
@commit_per_day ||= @commits.size / (@duration + 1) @commit_per_day ||= (@commits.size.to_f / (@duration + 1)).round(1)
end end
def collect_data def collect_data
......
...@@ -2913,7 +2913,11 @@ msgstr "" ...@@ -2913,7 +2913,11 @@ msgstr ""
msgid "Files (%{human_size})" msgid "Files (%{human_size})"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>" msgid "Fill in the fields below, turn on <strong>%{enable_label}</strong>, and press <strong>%{save_changes}</strong>"
=======
msgid "Filter"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
...@@ -4363,10 +4367,14 @@ msgstr "" ...@@ -4363,10 +4367,14 @@ msgstr ""
msgid "No files found." msgid "No files found."
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "No issues for the selected time period." msgid "No issues for the selected time period."
msgstr "" msgstr ""
msgid "No merge requests for the selected time period." msgid "No merge requests for the selected time period."
=======
msgid "No labels with such name or description"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
...@@ -4375,6 +4383,12 @@ msgstr "" ...@@ -4375,6 +4383,12 @@ msgstr ""
msgid "No messages were logged" msgid "No messages were logged"
msgstr "" msgstr ""
msgid "No other labels with such name or description"
msgstr ""
msgid "No prioritised labels with such name or description"
msgstr ""
msgid "No public groups" msgid "No public groups"
msgstr "" msgstr ""
...@@ -5987,6 +6001,9 @@ msgstr "" ...@@ -5987,6 +6001,9 @@ msgstr ""
msgid "Submit as spam" msgid "Submit as spam"
msgstr "" msgstr ""
msgid "Submit search"
msgstr ""
msgid "Subscribe" msgid "Subscribe"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Search for labels', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:label1) { create(:label, title: 'Foo', description: 'Lorem ipsum', project: project) }
let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) }
before do
project.add_maintainer(user)
sign_in(user)
visit project_labels_path(project)
end
it 'searches for label by title' do
fill_in 'label-search', with: 'Bar'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content(label2.title)
expect(page).to have_content(label2.description)
expect(page).not_to have_content(label1.title)
expect(page).not_to have_content(label1.description)
end
it 'searches for label by title' do
fill_in 'label-search', with: 'Lorem'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content(label1.title)
expect(page).to have_content(label1.description)
expect(page).not_to have_content(label2.title)
expect(page).not_to have_content(label2.description)
end
it 'shows nothing found message' do
fill_in 'label-search', with: 'nonexistent'
find('#label-search').native.send_keys(:enter)
expect(page).to have_content('No labels with such name or description')
expect(page).not_to have_content(label1.title)
expect(page).not_to have_content(label1.description)
expect(page).not_to have_content(label2.title)
expect(page).not_to have_content(label2.description)
end
context 'priority labels' do
let!(:label_priority) { create(:label_priority, label: label1, project: project) }
it 'searches for priority label' do
fill_in 'label-search', with: 'Foo'
find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do
expect(page).to have_content(label1.title)
expect(page).to have_content(label1.description)
end
page.within('.other-labels') do
expect(page).to have_content('No other labels with such name or description')
end
end
it 'searches for other label' do
fill_in 'label-search', with: 'Bar'
find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do
expect(page).to have_content('No prioritised labels with such name or description')
end
page.within('.other-labels') do
expect(page).to have_content(label2.title)
expect(page).to have_content(label2.description)
end
end
end
end
...@@ -14,7 +14,7 @@ describe LabelsFinder do ...@@ -14,7 +14,7 @@ describe LabelsFinder do
let(:project_4) { create(:project, :public) } let(:project_4) { create(:project, :public) }
let(:project_5) { create(:project, namespace: group_1) } let(:project_5) { create(:project, namespace: group_1) }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') } let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1', description: 'awesome label') }
let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') } let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') } let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') } let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
...@@ -196,5 +196,19 @@ describe LabelsFinder do ...@@ -196,5 +196,19 @@ describe LabelsFinder do
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
end end
context 'search by title and description' do
it 'returns labels with a partially matching title' do
finder = described_class.new(user, search: '(group)')
expect(finder.execute).to eq [group_label_1]
end
it 'returns labels with a partially matching description' do
finder = described_class.new(user, search: 'awesome')
expect(finder.execute).to eq [project_label_1]
end
end
end end
end end
...@@ -29,7 +29,7 @@ describe Gitlab::Graphs::Commits do ...@@ -29,7 +29,7 @@ describe Gitlab::Graphs::Commits do
context 'with commits from yesterday and today' do context 'with commits from yesterday and today' do
subject { described_class.new([commit2, commit1_yesterday]) } subject { described_class.new([commit2, commit1_yesterday]) }
describe '#commit_per_day' do describe '#commit_per_day' do
it { expect(subject.commit_per_day).to eq 1 } it { expect(subject.commit_per_day).to eq 1.0 }
end end
describe '#duration' do describe '#duration' do
......
...@@ -79,6 +79,46 @@ describe InternalId do ...@@ -79,6 +79,46 @@ describe InternalId do
end end
end end
describe '.track_greatest' do
let(:value) { 9001 }
subject { described_class.track_greatest(issue, scope, usage, value, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
end
it 'stores record attributes' do
subject
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
end
context 'with existing issues' do
before do
create(:issue, project: project)
described_class.delete_all
end
it 'still returns the last value to that of the given value' do
expect(subject).to eq(value)
end
end
context 'when value is less than the current last_value' do
it 'returns the current last_value' do
described_class.create!(**scope, usage: usage, last_value: 10_001)
expect(subject).to eq 10_001
end
end
end
describe '#increment_and_save!' do describe '#increment_and_save!' do
let(:id) { create(:internal_id) } let(:id) { create(:internal_id) }
subject { id.increment_and_save! } subject { id.increment_and_save! }
...@@ -103,4 +143,30 @@ describe InternalId do ...@@ -103,4 +143,30 @@ describe InternalId do
end end
end end
end end
describe '#track_greatest_and_save!' do
let(:id) { create(:internal_id) }
let(:new_last_value) { 9001 }
subject { id.track_greatest_and_save!(new_last_value) }
it 'returns new last value' do
expect(subject).to eq new_last_value
end
it 'saves the record' do
subject
expect(id.changed?).to be_falsey
end
context 'when new last value is lower than the max' do
it 'does not update the last value' do
id.update!(last_value: 10_001)
subject
expect(id.reload.last_value).to eq 10_001
end
end
end
end end
...@@ -139,4 +139,20 @@ describe Label do ...@@ -139,4 +139,20 @@ describe Label do
end end
end end
end end
describe '.search' do
let(:label) { create(:label, title: 'bug', description: 'incorrect behavior') }
it 'returns labels with a partially matching title' do
expect(described_class.search(label.title[0..2])).to eq([label])
end
it 'returns labels with a partially matching description' do
expect(described_class.search(label.description[0..5])).to eq([label])
end
it 'returns nothing' do
expect(described_class.search('feature')).to be_empty
end
end
end end
...@@ -1006,6 +1006,38 @@ describe API::Issues do ...@@ -1006,6 +1006,38 @@ describe API::Issues do
end end
end end
context 'an internal ID is provided' do
context 'by an admin' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", admin),
title: 'new issue', iid: 9001
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by an owner' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', iid: 9001
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by another user' do
it 'ignores the given internal ID' do
post api("/projects/#{project.id}/issues", user2),
title: 'new issue', iid: 9001
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).not_to eq 9001
end
end
end
it 'creates a new project issue' do it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user), post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3, title: 'new issue', labels: 'label, label2', weight: 3,
......
shared_examples 'gitlab projects import validations' do
context 'with an invalid path' do
let(:path) { '/invalid-path/' }
it 'returns an invalid project' do
project = subject.execute
expect(project).not_to be_persisted
expect(project).not_to be_valid
end
end
context 'with a valid path' do
it 'creates a project' do
project = subject.execute
expect(project).to be_persisted
expect(project).to be_valid
end
end
context 'override params' do
it 'stores them as import data when passed' do
project = described_class
.new(namespace.owner, import_params, description: 'Hello')
.execute
expect(project.import_data.data['override_params']['description']).to eq('Hello')
end
end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end
...@@ -60,6 +60,20 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true| ...@@ -60,6 +60,20 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
expect { subject }.not_to change { instance.public_send(internal_id_attribute) } expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
end end
context 'when the instance has an internal ID set' do
let(:internal_id) { 9001 }
it 'calls InternalId.update_last_value and sets the `last_value` to that of the instance' do
instance.send("#{internal_id_attribute}=", internal_id)
expect(InternalId)
.to receive(:track_greatest)
.with(instance, scope_attrs, usage, internal_id, any_args)
.and_return(internal_id)
subject
end
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment