Merge branch 'feature/group-level-labels' into 'master'

Add group level labels

## What does this MR do?

Add group level labels.

## Are there points in the code the reviewer needs to double check?

* `LabelsFinder`
* `Gitlab::Gfm::ReferenceRewriter`
* `Banzai::Filter::LabelReferenceFilter`

## Why was this MR needed?

We'll be adding more feature that allow you to do cross-project management of issues.

## Screenshots (if relevant)

* Group Labels
    ![Group Labels](/uploads/2244c06ad68eae4fb246fb4c81bf8060/2.png)

* Project Labels
    ![Project Labels](/uploads/c5839516d2282b51f7418d9dadbeceb4/1.png)

* Expanded references for group labels when moving issue to another project
    ![Expanded references for group labels when moving issue to another project](/uploads/0c9ab248a8420d4978d59349ae3d42e5/3.png)

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added
- Tests
  - [x] Added for this feature/bug
  - [ ] All builds are passing
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [ ] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

## What are the relevant issue numbers?

#19997

See merge request !6425
This commit is contained in:
Douwe Maan 2016-10-19 20:41:04 +00:00
commit 25ff16459c
116 changed files with 2005 additions and 464 deletions

View File

@ -21,6 +21,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix centering of custom header logos (Ashley Dumaine)
- Keep around commits only pipeline creation as pipeline data doesn't change over time
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425)
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive

View File

@ -168,6 +168,8 @@
shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true);
break;
case 'groups:labels:new':
case 'groups:labels:edit':
case 'projects:labels:new':
case 'projects:labels:edit':
new Labels();

View File

@ -66,7 +66,21 @@
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
}
}
.label-type {
display: block;
margin-bottom: 10px;
margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 100px;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
}
}
.label-description {
@ -209,6 +223,13 @@
}
.label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.label-subscribe-button-loading {
display: none;
}

View File

@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern
included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
@ -25,6 +26,10 @@ module IssuableActions
private
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!

View File

@ -1,9 +1,9 @@
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title)
labels = LabelsFinder.new(current_user).execute
respond_to do |format|
format.json { render json: labels }
format.json { render json: labels.as_json(only: [:id, :title, :color]) }
end
end
end

View File

@ -0,0 +1,92 @@
class Groups::LabelsController < Groups::ApplicationController
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
respond_to :html
def index
respond_to do |format|
format.html do
@labels = @group.labels.page(params[:page])
end
format.json do
available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
render json: available_labels.as_json(only: [:id, :title, :color])
end
end
end
def new
@label = @group.labels.new
@previous_labels_path = previous_labels_path
end
def create
@label = @group.labels.create(label_params)
if @label.valid?
redirect_to group_labels_path(@group)
else
render :new
end
end
def edit
@previous_labels_path = previous_labels_path
end
def update
if @label.update_attributes(label_params)
redirect_back_or_group_labels_path
else
render :edit
end
end
def destroy
@label.destroy
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), notice: 'Label was removed'
end
format.js
end
end
protected
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @group)
end
def authorize_read_labels!
return render_404 unless can?(current_user, :read_label, @group)
end
def label
@label ||= @group.labels.find(params[:id])
end
def label_params
params.require(:label).permit(:title, :description, :color)
end
def redirect_back_or_group_labels_path(options = {})
redirect_to previous_labels_path, options
end
def previous_labels_path
session.fetch(:previous_labels_path, fallback_path)
end
def fallback_path
group_labels_path(@group)
end
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
end

View File

@ -72,10 +72,10 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
})
end
end

View File

@ -76,9 +76,8 @@ module Projects
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
include: {
label: { only: [:id, :title, :description, :color, :priority] }
})
label: true
)
end
end
end

View File

@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@issues = @issues.page(params[:page])
@labels = @project.labels.where(title: params[:label_name])
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
respond_to do |format|
format.html

View File

@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
respond_to :js, :html
def index
@labels = @project.labels.unprioritized.page(params[:page])
@prioritized_labels = @project.labels.prioritized
@prioritized_labels = @available_labels.prioritized(@project)
@labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format|
format.html
format.json do
render json: @project.labels
render json: @available_labels.as_json(only: [:id, :title, :color])
end
end
end
@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
else
respond_to do |format|
format.html { render 'new' }
format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
render 'edit'
render :edit
end
end
@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy
@label.destroy
@labels = find_labels
respond_to do |format|
format.html do
@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority
respond_to do |format|
if label.update_attribute(:priority, nil)
label = @available_labels.find(params[:id])
if label.unprioritize!(project)
format.json { render json: label }
else
message = label.errors.full_messages.uniq.join('. ')
format.json { render json: { message: message }, status: :unprocessable_entity }
format.json { head :unprocessable_entity }
end
end
end
def set_priorities
Label.transaction do
params[:label_ids].each_with_index do |label_id, index|
label = @project.labels.find_by_id(label_id)
label.update_attribute(:priority, index) if label
available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
label_ids.each_with_index do |label_id, index|
label = @available_labels.find(label_id)
label.prioritize!(project, index)
end
end
@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :label
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
end
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end

View File

@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
@labels = @project.labels.where(title: params[:label_name])
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
respond_to do |format|
format.html
@ -575,6 +578,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
define_pipelines_vars
end

View File

@ -124,15 +124,12 @@ class IssuableFinder
def labels
return @labels if defined?(@labels)
if labels? && !filter_by_no_label?
@labels = Label.where(title: label_names)
if projects
@labels = @labels.where(project: projects)
@labels =
if labels? && !filter_by_no_label?
LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
else
Label.none
end
else
@labels = Label.none
end
end
def assignee?
@ -274,8 +271,10 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
if projects
items = items.where(labels: { project_id: projects })
label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
items = items.where(labels: { id: label_ids })
end
end
end

View File

@ -0,0 +1,92 @@
class LabelsFinder < UnionFinder
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
def execute(authorized_only: true)
@authorized_only = authorized_only
items = find_union(label_ids, Label)
items = with_title(items)
sort(items)
end
private
attr_reader :current_user, :params, :authorized_only
def label_ids
label_ids = []
if project
label_ids << project.group.labels if project.group.present?
label_ids << project.labels
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
end
label_ids
end
def sort(items)
items.reorder(title: :asc)
end
def with_title(items)
items = items.where(title: title) if title
items
end
def group_id
params[:group_id].presence
end
def project_id
params[:project_id].presence
end
def projects_ids
params[:project_ids].presence
end
def title
params[:title].presence || params[:name].presence
end
def project
return @project if defined?(@project)
if project_id
@project = find_project
else
@project = nil
end
@project
end
def find_project
if authorized_only
available_projects.find_by(id: project_id)
else
Project.find_by(id: project_id)
end
end
def projects
return @projects if defined?(@projects)
@projects = authorized_only ? available_projects : Project.all
@projects = @projects.in_namespace(group_id) if group_id
@projects = @projects.where(id: projects_ids) if projects_ids
@projects = @projects.reorder(nil)
@projects
end
def available_projects
@available_projects ||= ProjectsFinder.new.execute(current_user)
end
end

View File

@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label
#
# label - Label object to link to
# project - Project object which will be used as the context for the label's
# link. If omitted, defaults to `@project`, or the label's own
# project.
# subject - Project/Group object which will be used as the context for the
# label's link. If omitted, defaults to the label's own group/project.
# type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
@ -15,15 +14,14 @@ module LabelsHelper
#
# Examples:
#
# # Allow the generated link to use the label's own project
# # Allow the generated link to use the label's own subject
# link_to_label(label)
#
# # Force the generated link to use @project
# @project = Project.first
# link_to_label(label)
# # Force the generated link to use a provided group
# link_to_label(label, subject: Group.last)
#
# # Force the generated link to use a provided project
# link_to_label(label, project: Project.last)
# link_to_label(label, subject: Project.last)
#
# # Force the generated link to point to merge requests instead of issues
# link_to_label(label, type: :merge_request)
@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project
link = label_filter_path(project, label, type: type)
def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
link = label_filter_path(subject || label.subject, label, type: type)
if block_given?
link_to link, class: css_class, &block
@ -43,15 +40,40 @@ module LabelsHelper
end
end
def label_filter_path(project, label, type: issue)
send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace,
project,
label_name: [label.name])
def label_filter_path(subject, label, type: :issue)
case subject
when Group
send("#{type.to_s.pluralize}_group_path",
subject,
label_name: [label.name])
when Project
send("namespace_project_#{type.to_s.pluralize}_path",
subject.namespace,
subject,
label_name: [label.name])
end
end
def project_label_names
@project.labels.pluck(:title)
def edit_label_path(label)
case label
when GroupLabel then edit_group_label_path(label.group, label)
when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def destroy_label_path(label)
case label
when GroupLabel then group_label_path(label.group, label)
when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def toggle_subscription_data(label)
return unless label.is_a?(ProjectLabel)
{
url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
}
end
def render_colored_label(label, label_suffix = '', tooltip: true)
@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe
end
def render_colored_cross_project_label(label, tooltip: true)
label_suffix = label.project.name_with_namespace
def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix, tooltip: tooltip)
end
@ -115,7 +137,10 @@ module LabelsHelper
end
def labels_filter_path
return group_labels_path(@group, :json) if @group
project = @target_project || @project
if project
namespace_project_labels_path(project.namespace, project, :json)
else
@ -124,11 +149,24 @@ module LabelsHelper
end
def label_subscription_status(label)
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
end
end
def label_subscription_toggle_button_text(label)
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
end
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
when ProjectLabel then 'Remove this label? Are you sure?'
end
end
# Required for Banzai::Filter::LabelReferenceFilter

View File

@ -145,8 +145,14 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
condition_field = "#{table_name}.id"
highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
params = {
target_type: name,
target_column: "#{table_name}.id",
project_column: "#{table_name}.#{project_foreign_key}",
excluded_labels: excluded_labels
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
@ -230,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title)
end
def remove_labels
labels.delete_all
end
def add_labels_by_names(label_names)
label_names.each do |label_name|
label = project.labels.create_with(color: Label::DEFAULT_COLOR).
find_or_create_by(title: label_name.strip)
self.labels << label
end
end
# Convert this Issuable class name to a format usable by Ability definitions
#
# Examples:

View File

@ -38,11 +38,13 @@ module Sortable
private
def highest_label_priority(object_types, condition_field, excluded_labels: [])
query = Label.select(Label.arel_table[:priority].minimum).
def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
query = Label.select(LabelPriority.arel_table[:priority].minimum).
left_join_priorities.
joins(:label_links).
where(label_links: { target_type: object_types }).
where("label_links.target_id = #{condition_field}").
where("label_priorities.project_id = #{project_column}").
where(label_links: { target_type: target_type }).
where("label_links.target_id = #{target_column}").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?

View File

@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
has_many :labels, class_name: 'GroupLabel'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects

11
app/models/group_label.rb Normal file
View File

@ -0,0 +1,11 @@
class GroupLabel < Label
belongs_to :group
validates :group, presence: true
alias_attribute :subject, :group
def to_reference(source_project = nil, target_project = nil, format: :id)
super(source_project, target_project, format: format)
end
end

View File

@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.project_foreign_key
'project_id'
end
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base
def check_for_spam?
project.public?
end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
only: [:id, :title, :description, :color],
methods: [:text_color]
)
end
end
end
end

View File

@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR
belongs_to :project
has_many :lists, dependent: :destroy
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles
validates :title,
presence: true,
format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
default_scope { order(title: :asc) }
scope :templates, -> { where(template: true) }
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
def self.prioritized
where.not(priority: nil).reorder(:priority, :title)
def self.prioritized(project)
joins(:priorities)
.where(label_priorities: { project_id: project })
.reorder('label_priorities.priority ASC, labels.title ASC')
end
def self.unprioritized
where(priority: nil)
def self.unprioritized(project)
labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
join_sources
joins(label_priorities).where(priorities[:priority].eq(nil))
end
def self.left_join_priorities
labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id])).
join_sources
joins(label_priorities)
end
alias_attribute :name, :title
@ -77,40 +92,30 @@ class Label < ActiveRecord::Base
nil
end
##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
# Examples:
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
def to_reference(from_project = nil, format: :id)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project)
project.to_reference + reference
else
reference
end
def open_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def open_issues_count(user = nil)
issues.visible_to_user(user).opened.count
def closed_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
end
def closed_issues_count(user = nil)
issues.visible_to_user(user).closed.count
def open_merge_requests_count(user = nil, project = nil)
merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def open_merge_requests_count
merge_requests.opened.count
def prioritize!(project, value)
label_priority = priorities.find_or_initialize_by(project_id: project.id)
label_priority.priority = value
label_priority.save!
end
def unprioritize!(project)
priorities.where(project: project).delete_all
end
def priority(project)
priorities.find_by(project: project).try(:priority)
end
def template?
@ -118,15 +123,61 @@ class Label < ActiveRecord::Base
end
def text_color
LabelsHelper::text_color_for_bg(self.color)
LabelsHelper.text_color_for_bg(self.color)
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
# Examples:
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
def to_reference(source_project = nil, target_project = nil, format: :id)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(source_project, target_project)
source_project.to_reference + reference
else
reference
end
end
def as_json(options = {})
super(options).tap do |json|
json[:priority] = priority(options[:project]) if options.has_key?(:project)
end
end
private
def cross_project_reference?(source_project, target_project)
source_project && target_project && source_project != target_project
end
def issues_count(user, params = {})
IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end
def merge_requests_count(user, params = {})
MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end
def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
@ -137,10 +188,6 @@ class Label < ActiveRecord::Base
end
end
def nullify_priority
self.priority = nil if priority.blank?
end
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end

View File

@ -0,0 +1,8 @@
class LabelPriority < ActiveRecord::Base
belongs_to :project
belongs_to :label
validates :project, :label, :priority, presence: true
validates :label_id, uniqueness: { scope: :project_id }
validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end

View File

@ -26,6 +26,17 @@ class List < ActiveRecord::Base
label? ? label.name : list_type.humanize
end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
)
end
end
end
private
def can_be_destroyed

View File

@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.project_foreign_key
'target_project_id'
end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of

View File

@ -107,7 +107,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
@ -388,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count
end
end
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
end
end
def lfs_enabled?
@ -733,10 +737,8 @@ class Project < ActiveRecord::Base
def create_labels
Label.templates.each do |label|
label = label.dup
label.template = nil
label.project_id = self.id
label.save
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
Labels::FindOrCreateService.new(owner, self, params).execute
end
end

View File

@ -0,0 +1,34 @@
class ProjectLabel < Label
MAX_NUMBER_OF_PRIORITIES = 1
belongs_to :project
validates :project, presence: true
validate :permitted_numbers_of_priorities
validate :title_must_not_exist_at_group_level
delegate :group, to: :project, allow_nil: true
alias_attribute :subject, :project
def to_reference(target_project = nil, format: :id)
super(project, target_project, format: format)
end
private
def title_must_not_exist_at_group_level
return unless group.present? && title_changed?
if group.labels.with_title(self.title).exists?
errors.add(:title, :label_already_exists_at_group_level, group: group.name)
end
end
def permitted_numbers_of_priorities
if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
errors.add(:priorities, 'Number of permitted priorities exceeded')
end
end
end

View File

@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority
highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
params = {
target_type: ['Issue', 'MergeRequest'],
target_column: "todos.target_id",
project_column: "todos.project_id"
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).

View File

@ -0,0 +1,5 @@
class GroupLabelPolicy < BasePolicy
def rules
delegate! @subject.group
end
end

View File

@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy
if master
can! :create_projects
can! :admin_milestones
can! :admin_label
end
# Only group owner and administrators can admin group

View File

@ -0,0 +1,5 @@
class ProjectLabelPolicy < BasePolicy
def rules
delegate! @subject.project
end
end

View File

@ -3,7 +3,7 @@ module Boards
class CreateService < BaseService
def execute(board)
List.transaction do
label = project.labels.find(params[:label_id])
label = available_labels.find(params[:label_id])
position = next_position(board)
create_list(board, label, position)
@ -12,6 +12,10 @@ module Boards
private
def available_labels
LabelsFinder.new(current_user, project_id: project.id).execute
end
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ

View File

@ -19,8 +19,7 @@ module Boards
end
def find_or_create_label(params)
project.labels.create_with(color: params[:color])
.find_or_create_by(name: params[:name])
::Labels::FindOrCreateService.new(current_user, project, params).execute
end
def label_params

View File

@ -80,17 +80,18 @@ class IssuableBaseService < BaseService
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[key] = project.labels.where(id: params[key]).pluck(:id)
params[key] = available_labels.where(id: params[key]).pluck(:id)
end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
project.labels.create_with(color: Label::DEFAULT_COLOR)
.find_or_create_by(title: label_name.strip)
.id
params[:label_ids] = labels.split(',').map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label.id
end
end
@ -111,6 +112,10 @@ class IssuableBaseService < BaseService
new_label_ids
end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).

View File

@ -52,8 +52,12 @@ module Issues
end
def cloneable_label_ids
@new_project.labels
.where(title: @old_issue.labels.pluck(:title)).pluck(:id)
params = {
project_id: @new_project.id,
title: @old_issue.labels.pluck(:title)
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
end
def cloneable_milestone_id

View File

@ -0,0 +1,33 @@
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
@current_user = current_user
@group = project.group
@project = project
@params = params.dup
end
def execute
find_or_create_label
end
private
attr_reader :current_user, :group, :project, :params
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute
end
def find_or_create_label
new_label = available_labels.find_by(title: title)
new_label ||= project.labels.create(params)
new_label
end
def title
params[:title] || params[:name]
end
end
end

View File

@ -0,0 +1,78 @@
# Labels::TransferService class
#
# User for recreate the missing group labels at project level
#
module Labels
class TransferService
def initialize(current_user, old_group, project)
@current_user = current_user
@old_group = old_group
@project = project
end
def execute
return unless old_group.present?
Label.transaction do
labels_to_transfer.find_each do |label|
new_label_id = find_or_create_label!(label)
next if new_label_id == label.id
update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
end
end
end
private
attr_reader :current_user, :old_group, :project
def labels_to_transfer
label_ids = []
label_ids << group_labels_applied_to_issues.select(:id)
label_ids << group_labels_applied_to_merge_requests.select(:id)
union = Gitlab::SQL::Union.new(label_ids)
Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
end
def group_labels_applied_to_issues
Label.joins(:issues).
where(
issues: { project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def group_labels_applied_to_merge_requests
Label.joins(:merge_requests).
where(
merge_requests: { target_project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def find_or_create_label!(label)
params = label.attributes.slice('title', 'description', 'color')
new_label = FindOrCreateService.new(current_user, project, params).execute
new_label.id
end
def update_label_links(labels, old_label_id:, new_label_id:)
LabelLink.joins(:label).
merge(labels).
where(label_id: old_label_id).
update_all(label_id: new_label_id)
end
def update_label_priorities(old_label_id:, new_label_id:)
LabelPriority.where(project_id: project.id, label_id: old_label_id).
update_all(label_id: new_label_id)
end
end
end

View File

@ -13,7 +13,7 @@ module Projects
end
def labels
@project.labels.select([:title, :color])
LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
end
def commands(noteable, type)

View File

@ -28,6 +28,7 @@ module Projects
Project.transaction do
old_path = project.path_with_namespace
old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
@ -57,6 +58,9 @@ module Projects
# Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
# clear project cached events
project.reset_events_cache

View File

@ -116,8 +116,10 @@ module SlashCommands
desc 'Add label(s)'
params '~label1 ~"label 2"'
condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.labels.any?
available_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
@ -248,7 +250,7 @@ module SlashCommands
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
label_ids_by_reference | labels_ids_by_name
end

View File

@ -0,0 +1,2 @@
- if @group.labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)

View File

@ -0,0 +1,7 @@
- page_title 'Edit', @label.name, 'Labels'
%h3.page-title
Edit Label
%hr
= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path

View File

@ -0,0 +1,20 @@
- page_title 'Labels'
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
.nav-controls
- if can?(current_user, :admin_label, @group)
= link_to new_group_label_path(@group), class: "btn btn-new" do
New label
.labels
.other-labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
No labels created yet.

View File

@ -0,0 +1,8 @@
- page_title 'New Label'
- header_title group_title(@group, 'Labels', group_labels_path(@group))
%h3.page-title
New Label
%hr
= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path

View File

@ -13,6 +13,10 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
= nav_link(controller: [:group, :labels]) do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span

View File

@ -50,7 +50,7 @@
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
= link_to_label(label, project: issue.project)
= link_to_label(label, subject: issue.project)
- if issue.tasks?
&nbsp;
%span.task-status

View File

@ -1,50 +0,0 @@
- label_css_id = dom_id(label)
%li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
%li
= link_to_label(label, type: :merge_request) do
= pluralize label.open_merge_requests_count, 'merge request'
%li
= link_to_label(label) do
= pluralize label.open_issues_count(current_user), 'open issue'
- if current_user
%li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
%a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
%li
= link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
%li
= link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_merge_requests_count, 'merge request'
= link_to_label(label, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_issues_count(current_user), 'open issue'
- if current_user
.label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
%button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span.sr-only= label_subscription_toggle_button_text(label)
= icon('eye', class: 'label-subscribe-button-icon')
= icon('spinner spin', class: 'label-subscribe-button-loading')
- if can? current_user, :admin_label, @project
= link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
= link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
- if current_user
:javascript
new Subscription('##{dom_id(label)} .label-subscription');

View File

@ -1,2 +1,2 @@
- if @project.labels.size == 0
- if @labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)

View File

@ -6,4 +6,4 @@
%h3.page-title
Edit Label
%hr
= render 'form'
= render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project)

View File

@ -16,21 +16,22 @@
.labels
- if can?(current_user, :admin_label, @project)
-# Only show it in the first page
- hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
%h5 Prioritized Labels
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
= render @prioritized_labels
= render partial: 'shared/label', collection: @prioritized_labels, as: :label
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
= render @labels
%ul.content-list.manage-labels-list.js-other-labels
- if @labels.present?
= render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
- if @labels.blank?
.nothing-here-block
- if can?(current_user, :admin_label, @project)
Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.

View File

@ -6,4 +6,4 @@
%h3.page-title
New Label
%hr
= render 'form'
= render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project)

View File

@ -62,7 +62,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, project: merge_request.project, type: 'merge_request')
= link_to_label(label, subject: merge_request.project, type: :merge_request)
- if merge_request.tasks?
&nbsp;
%span.task-status

View File

@ -0,0 +1,53 @@
- label_css_id = dom_id(label)
- open_issues_count = label.open_issues_count(current_user, @project)
- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
%li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
%li
= link_to_label(label, subject: @project, type: :merge_request) do
= pluralize open_merge_requests_count, 'merge request'
%li
= link_to_label(label, subject: @project) do
= pluralize open_issues_count, 'open issue'
- if current_user
%li.label-subscription{ data: toggle_subscription_data(label) }
%a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span= label_subscription_toggle_button_text(label)
- if can?(current_user, :admin_label, label)
%li
= link_to 'Edit', edit_label_path(label)
%li
= link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize open_merge_requests_count, 'merge request'
= link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- if current_user
.label-subscription.inline{ data: toggle_subscription_data(label) }
%button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span.sr-only= label_subscription_toggle_button_text(label)
= icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
= icon('spinner spin', class: 'label-subscribe-button-loading')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
= link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
- if current_user && label.is_a?(ProjectLabel)
:javascript
new Subscription('##{dom_id(label)} .label-subscription');

View File

@ -3,13 +3,16 @@
.draggable-handler
= icon('bars')
.js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
dom_id: dom_id(label) } }
dom_id: dom_id(label), type: label.type } }
%button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
= icon('star-o')
%button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
= icon('star')
%span.label-name
= link_to_label(label, tooltip: false)
= link_to_label(label, subject: @project, tooltip: false)
- if defined?(@project) && @project.group.present?
%span.label-type
= label.model_name.human.titleize
- if label.description
%span.label-description
= markdown_field(label, :description)

View File

@ -1,5 +1,5 @@
- labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
= link_to_label(label, css_class: 'btn btn-transparent')
= link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times")

View File

@ -78,11 +78,10 @@
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- if !@labels.nil?
.row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
- if @labels.any?
= render "shared/labels_row", labels: @labels
- has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels
= render 'shared/labels_row', labels: @labels
:javascript
new UsersSelect();

View File

@ -95,7 +95,7 @@
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- has_labels = issuable.project.labels.any?
- has_labels = @labels && @labels.any?
= f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
= f.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }

View File

@ -107,7 +107,7 @@
= dropdown_content do
.js-due-date-calendar
- if issuable.project.labels.any?
- if @labels && @labels.any?
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }

View File

@ -1,4 +1,4 @@
= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
.form-group
@ -30,4 +30,4 @@
= f.submit 'Save changes', class: 'btn btn-save js-save-button'
- else
= f.submit 'Create Label', class: 'btn btn-create js-save-button'
= link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
= link_to 'Cancel', back_path, class: 'btn btn-cancel'

View File

@ -5,6 +5,7 @@ en:
hello: "Hello world"
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})"

View File

@ -28,5 +28,7 @@ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
resources :labels, except: [:show], constraints: { id: /\d+/ }
end
end

View File

@ -0,0 +1,14 @@
class AddTypeToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.'
def change
add_column :labels, :type, :string
update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
query.where(table[:project_id].not_eq(nil))
end
end
end

View File

@ -0,0 +1,13 @@
class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_column :labels, :group_id, :integer
add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
add_concurrent_index :labels, :group_id
end
end

View File

@ -0,0 +1,25 @@
class CreateLabelPriorities < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration adds foreign keys'
disable_ddl_transaction!
def up
create_table :label_priorities do |t|
t.references :project, foreign_key: { on_delete: :cascade }, null: false
t.references :label, foreign_key: { on_delete: :cascade }, null: false
t.integer :priority, null: false
t.timestamps null: false
end
add_concurrent_index :label_priorities, [:project_id, :label_id], unique: true
add_concurrent_index :label_priorities, :priority
end
def down
drop_table :label_priorities
end
end

View File

@ -0,0 +1,32 @@
class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration removes duplicated labels.'
disable_ddl_transaction!
def up
select_all('SELECT title, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
label_title = quote_string(label['title'])
duplicated_ids = select_all("SELECT id FROM labels WHERE title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
label_id = duplicated_ids.first
duplicated_ids.delete(label_id)
execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})")
execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})")
end
remove_index :labels, column: :project_id if index_exists?(:labels, :project_id)
remove_index :labels, column: :title if index_exists?(:labels, :title)
add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true
end
def down
remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true)
add_concurrent_index :labels, :project_id
add_concurrent_index :labels, :title
end
end

View File

@ -0,0 +1,36 @@
class MigrateLabelsPriority < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.'
disable_ddl_transaction!
def up
execute <<-EOF.strip_heredoc
INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at)
SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW()
FROM labels
WHERE labels.project_id IS NOT NULL
AND labels.priority IS NOT NULL;
EOF
end
def down
if Gitlab::Database.mysql?
execute <<-EOF.strip_heredoc
UPDATE labels
INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id
SET labels.priority = label_priorities.priority;
EOF
else
execute <<-EOF.strip_heredoc
UPDATE labels
SET priority = label_priorities.priority
FROM label_priorities
WHERE labels.id = label_priorities.label_id
AND labels.project_id = label_priorities.project_id;
EOF
end
end
end

View File

@ -0,0 +1,17 @@
class RemovePriorityFromLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration removes an existing column'
disable_ddl_transaction!
def up
remove_column :labels, :priority, :integer, index: true
end
def down
add_column :labels, :priority, :integer
add_concurrent_index :labels, :priority
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161017095000) do
ActiveRecord::Schema.define(version: 20161018024550) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -519,6 +519,17 @@ ActiveRecord::Schema.define(version: 20161017095000) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
create_table "label_priorities", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "label_id", null: false
t.integer "priority", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
create_table "labels", force: :cascade do |t|
t.string "title"
t.string "color"
@ -527,13 +538,13 @@ ActiveRecord::Schema.define(version: 20161017095000) do
t.datetime "updated_at"
t.boolean "template", default: false
t.string "description"
t.integer "priority"
t.text "description_html"
t.string "type"
t.integer "group_id"
end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@ -1213,6 +1224,9 @@ ActiveRecord::Schema.define(version: 20161017095000) do
add_foreign_key "boards", "projects"
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade

View File

@ -22,7 +22,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
| 8.12.0 to current | 0.1.4 |
| 8.13.0 to current | 0.1.5 |
| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |

View File

@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end
step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do
page.within "#project_label_#{bug_label.id}" do
first(:link, 'Delete').click
end
end

View File

@ -65,8 +65,8 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
unless user_project.labels.exists?(params[:label_id])
render_api_error!({ error: "Label not found!" }, 400)
unless available_labels.exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)

View File

@ -71,6 +71,10 @@ module API
@project ||= find_project(params[:id])
end
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id)
@ -118,7 +122,7 @@ module API
end
def find_project_label(id)
label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
label || not_found!('Label')
end
@ -197,16 +201,11 @@ module API
def validate_label_params(params)
errors = {}
if params[:labels].present?
params[:labels].split(',').each do |label_name|
label = user_project.labels.create_with(
color: Label::DEFAULT_COLOR).find_or_initialize_by(
title: label_name.strip)
params[:labels].to_s.split(',').each do |label_name|
label = available_labels.find_or_initialize_by(title: label_name.strip)
next if label.valid?
if label.invalid?
errors[label.title] = label.errors
end
end
errors[label.title] = label.errors
end
errors

View File

@ -11,7 +11,7 @@ module API
# Example Request:
# GET /projects/:id/labels
get ':id/labels' do
present user_project.labels, with: Entities::Label, current_user: current_user
present available_labels, with: Entities::Label, current_user: current_user
end
# Creates a new label

View File

@ -86,14 +86,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
attrs[:labels] = params[:labels] if params[:labels]
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
if merge_request.valid?
# Find or create labels and attach to issue
if params[:labels].present?
merge_request.add_labels_by_names(params[:labels].split(","))
end
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
@ -195,15 +192,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
attrs[:labels] = params[:labels] if params[:labels]
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
if merge_request.valid?
# Find or create labels and attach to issue
unless params[:labels].nil?
merge_request.remove_labels
merge_request.add_labels_by_names(params[:labels].split(","))
end
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors

View File

@ -9,7 +9,7 @@ module Banzai
end
def find_object(project, id)
project.labels.find(id)
find_labels(project).find(id)
end
def self.references_in(text, pattern = Label.reference_pattern)
@ -35,7 +35,11 @@ module Banzai
return unless project
label_params = label_params(label_id, label_name)
project.labels.find_by(label_params)
find_labels(project).find_by(label_params)
end
def find_labels(project)
LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false)
end
# Parameters to pass to `Label.find_by` based on the given arguments
@ -60,13 +64,50 @@ module Banzai
end
def object_link_text(object, matches)
if context[:project] == object.project
LabelsHelper.render_colored_label(object)
if same_group?(object) && namespace_match?(matches)
render_same_project_label(object)
elsif same_project?(object)
render_same_project_label(object)
else
LabelsHelper.render_colored_cross_project_label(object)
render_cross_project_label(object, matches)
end
end
def same_group?(object)
object.is_a?(GroupLabel) && object.group == project.group
end
def namespace_match?(matches)
matches[:project].blank? || matches[:project] == project.path_with_namespace
end
def same_project?(object)
object.is_a?(ProjectLabel) && object.project == project
end
def user
context[:current_user] || context[:author]
end
def project
context[:project]
end
def render_same_project_label(object)
LabelsHelper.render_colored_label(object)
end
def render_cross_project_label(object, matches)
source_project =
if matches[:project]
Project.find_with_namespace(matches[:project])
else
object.project
end
LabelsHelper.render_colored_cross_project_label(object, source_project)
end
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end

View File

@ -74,8 +74,8 @@ module Gitlab
end
def create_label(name)
color = nice_label_color(name)
Label.create!(project_id: project.id, title: name, color: color)
params = { title: name, color: nice_label_color(name) }
::Labels::FindOrCreateService.new(project.owner, project, params).execute
end
def user_info(person_id)
@ -122,25 +122,21 @@ module Gitlab
author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
issue = Issue.create!(
project_id: project.id,
title: bug['sTitle'],
description: body,
author_id: author_id,
assignee_id: assignee_id,
state: bug['fOpen'] == 'true' ? 'opened' : 'closed'
iid: bug['ixBug'],
project_id: project.id,
title: bug['sTitle'],
description: body,
author_id: author_id,
assignee_id: assignee_id,
state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
created_at: date,
updated_at: DateTime.parse(bug['dtLastUpdated'])
)
issue.add_labels_by_names(labels)
if issue.iid != bug['ixBug']
issue.update_attribute(:iid, bug['ixBug'])
end
issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
issue.update_attribute(:created_at, date)
last_update = DateTime.parse(bug['dtLastUpdated'])
issue.update_attribute(:updated_at, last_update)
end
end

View File

@ -58,7 +58,7 @@ module Gitlab
referable = find_referable(reference)
return reference unless referable
cross_reference = referable.to_reference(target_project)
cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
new_text = before + cross_reference + after
@ -72,6 +72,14 @@ module Gitlab
extractor.all.first
end
def build_cross_reference(referable, target_project)
if referable.respond_to?(:project)
referable.to_reference(target_project)
else
referable.to_reference(@source_project, target_project)
end
end
def substitution_valid?(substituted)
@original_html == markdown(substituted)
end

View File

@ -14,9 +14,13 @@ module Gitlab
end
def create!
project.labels.find_or_create_by!(title: title) do |label|
label.color = color
end
params = attributes.except(:project)
service = ::Labels::FindOrCreateService.new(project.owner, project, params)
label = service.execute
raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
label
end
private

View File

@ -92,19 +92,17 @@ module Gitlab
end
issue = Issue.create!(
project_id: project.id,
title: raw_issue["title"],
description: body,
author_id: project.creator_id,
assignee_id: assignee_id,
state: raw_issue["state"] == "closed" ? "closed" : "opened"
iid: raw_issue['id'],
project_id: project.id,
title: raw_issue['title'],
description: body,
author_id: project.creator_id,
assignee_id: assignee_id,
state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
)
issue.add_labels_by_names(labels)
if issue.iid != raw_issue["id"]
issue.update_attribute(:iid, raw_issue["id"])
end
issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
end
@ -236,8 +234,8 @@ module Gitlab
end
def create_label(name)
color = nice_label_color(name)
Label.create!(project_id: project.id, name: name, color: color)
params = { name: name, color: nice_label_color(name) }
::Labels::FindOrCreateService.new(project.owner, project, params).execute
end
def format_content(raw_content)

View File

@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
VERSION = '0.1.4'
VERSION = '0.1.5'
FILENAME_LIMIT = 50
def export_path(relative_path:)

View File

@ -1,7 +1,7 @@
module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value|

View File

@ -1,6 +1,7 @@
# Model relationships to be included in the project import/export
project_tree:
- :labels
- labels:
:priorities
- milestones:
- :events
- issues:
@ -9,7 +10,8 @@ project_tree:
- :author
- :events
- label_links:
- :label
- label:
:priorities
- milestone:
- :events
- snippets:
@ -26,7 +28,8 @@ project_tree:
- :merge_request_diff
- :events
- label_links:
- :label
- label:
:priorities
- milestone:
- :events
- pipelines:
@ -71,6 +74,10 @@ excluded_attributes:
- :awardable_id
methods:
labels:
- :type
label:
- :type
statuses:
- :type
services:

View File

@ -65,11 +65,17 @@ module Gitlab
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
def parse_hash(value)
return nil if already_contains_methods?(value)
@attributes_finder.parse(value) do |hash|
{ include: hash_or_merge(value, hash) }
end
end
def already_contains_methods?(value)
value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
end
# Adds new model configuration to an existing hash with key +current_key+
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
#

View File

@ -110,7 +110,7 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: relation_hash,
relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
@ -118,6 +118,10 @@ module Gitlab
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
def parsed_relation_hash(relation_hash)
relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
end
end
end
end

View File

@ -9,7 +9,10 @@ module Gitlab
builds: 'Ci::Build',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
push_access_levels: 'ProtectedBranch::PushAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
@ -19,9 +22,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
FINDER_ATTRIBUTES = %w[title project_id].freeze
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
def self.create(*args)
new(*args).create
@ -56,6 +57,8 @@ module Gitlab
update_user_references
update_project_references
handle_group_label if group_label?
reset_ci_tokens if @relation_name == 'Ci::Trigger'
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
@ -123,6 +126,20 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
def group_label?
@relation_hash['type'] == 'GroupLabel'
end
def handle_group_label
# If there's no group, move the label to a project label
if @relation_hash['group_id']
@relation_hash['project_id'] = nil
@relation_name = :group_label
else
@relation_hash['type'] = 'ProjectLabel'
end
end
def reset_ci_tokens
return unless Gitlab::ImportExport.reset_tokens?
@ -171,11 +188,9 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
events = parsed_relation_hash.delete('events')
attribute_hash = attribute_hash_for(['events', 'priorities'])
unless events.blank?
existing_object.assign_attributes(events: events)
end
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object
else
@ -184,14 +199,22 @@ module Gitlab
end
end
def attribute_hash_for(attributes)
attributes.inject({}) do |hash, value|
hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
hash
end
end
def existing_object
@existing_object ||=
begin
finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
finder_hash = parsed_relation_hash.slice(*finder_attributes)
existing_object = relation_class.find_or_create_by(finder_hash)
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update(parsed_relation_hash)
existing_object.update!(parsed_relation_hash)
existing_object
end
end

View File

@ -18,8 +18,8 @@ module Gitlab
{ title: "enhancement", color: green }
]
labels.each do |label|
project.labels.create(label)
labels.each do |params|
::Labels::FindOrCreateService.new(project.owner, project).execute(params)
end
end
end

View File

@ -1,52 +1,73 @@
require 'spec_helper'
describe Projects::LabelsController do
let(:project) { create(:project) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
sign_in(user)
end
describe 'GET #index' do
def create_label(attributes)
create(:label, attributes.merge(project: project))
end
let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') }
let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') }
let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') }
let!(:label_4) { create(:label, project: project, title: 'Label 4') }
let!(:label_5) { create(:label, project: project, title: 'Label 5') }
let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') }
let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
before do
15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
5.times { |i| create_label(title: "label #{100 - i}") }
get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
create(:label_priority, project: project, label: group_label_1, priority: 3)
create(:label_priority, project: project, label: group_label_2, priority: 1)
end
context '@prioritized_labels' do
let(:prioritized_labels) { assigns(:prioritized_labels) }
before do
list_labels
end
it 'contains only prioritized labels' do
expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
it 'does not include labels without priority' do
list_labels
expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5)
end
it 'is sorted by priority, then label title' do
priorities_and_titles = prioritized_labels.pluck(:priority, :title)
expect(priorities_and_titles.sort).to eq(priorities_and_titles)
expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2]
end
end
context '@labels' do
let(:labels) { assigns(:labels) }
it 'contains only unprioritized labels' do
expect(labels).to all(have_attributes(priority: nil))
end
it 'is sorted by label title' do
titles = labels.pluck(:title)
list_labels
expect(titles.sort).to eq(titles)
expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5]
end
it 'does not include labels with priority' do
list_labels
expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2)
end
it 'does not include group labels when project does not belong to a group' do
project.update(namespace: create(:namespace))
list_labels
expect(assigns(:labels)).not_to include(group_label_3, group_label_4)
end
end
def list_labels
get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
end
end
end

View File

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :label_priority do
project factory: :empty_project
label
sequence(:priority)
end
end

View File

@ -1,7 +1,23 @@
FactoryGirl.define do
factory :label do
factory :label, class: ProjectLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
project
transient do
priority nil
end
after(:create) do |label, evaluator|
if evaluator.priority
label.priorities.create(project: label.project, priority: evaluator.priority)
end
end
end
factory :group_label, class: GroupLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
group
end
end

View File

@ -68,5 +68,15 @@ FactoryGirl.define do
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
factory :labeled_merge_request do
transient do
labels []
end
after(:create) do |merge_request, evaluator|
merge_request.update_attributes(labels: evaluator.labels)
end
end
end
end

View File

@ -3,18 +3,56 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do
include WaitForAjax
context 'when project belongs to user' do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
let!(:feature) { create(:group_label, group: group, title: 'feature') }
scenario 'user can prioritize a label', js: true do
bug = create(:label, title: 'bug')
wontfix = create(:label, title: 'wontfix')
project.labels << bug
project.labels << wontfix
context 'when user belongs to project team' do
before do
project.team << [user, :developer]
login_as user
end
scenario 'user can prioritize a group label', js: true do
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet')
page.within('.other-labels') do
all('.js-toggle-priority')[1].click
wait_for_ajax
expect(page).not_to have_content('feature')
end
page.within('.prioritized-labels') do
expect(page).not_to have_content('No prioritized labels yet')
expect(page).to have_content('feature')
end
end
scenario 'user can unprioritize a group label', js: true do
create(:label_priority, project: project, label: feature, priority: 1)
visit namespace_project_labels_path(project.namespace, project)
page.within('.prioritized-labels') do
expect(page).to have_content('feature')
first('.js-toggle-priority').click
wait_for_ajax
expect(page).not_to have_content('bug')
end
page.within('.other-labels') do
expect(page).to have_content('feature')
end
end
scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet')
@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do
end
end
scenario 'user can unprioritize a label', js: true do
bug = create(:label, title: 'bug', priority: 1)
wontfix = create(:label, title: 'wontfix')
scenario 'user can unprioritize a project label', js: true do
create(:label_priority, project: project, label: bug, priority: 1)
project.labels << bug
project.labels << wontfix
login_as user
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('bug')
page.within('.prioritized-labels') do
expect(page).to have_content('bug')
first('.js-toggle-priority').click
wait_for_ajax
expect(page).not_to have_content('bug')
@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do
end
scenario 'user can sort prioritized labels and persist across reloads', js: true do
bug = create(:label, title: 'bug', priority: 1)
wontfix = create(:label, title: 'wontfix', priority: 2)
create(:label_priority, project: project, label: bug, priority: 1)
create(:label_priority, project: project, label: feature, priority: 2)
project.labels << bug
project.labels << wontfix
login_as user
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
expect(page).to have_content 'wontfix'
# Sort labels
find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix')
expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do
wait_for_ajax
page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix')
expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
end
@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do
context 'as a guest' do
it 'does not prioritize labels' do
user = create(:user)
guest = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
create(:label, title: 'bug')
login_as guest
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end
context 'as a non signed in user' do
it 'does not prioritize labels' do
user = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
create(:label, title: 'bug')
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end

View File

@ -0,0 +1,69 @@
require 'spec_helper'
describe LabelsFinder do
describe '#execute' do
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
let(:project_1) { create(:empty_project, namespace: group_1) }
let(:project_2) { create(:empty_project, namespace: group_2) }
let(:project_3) { create(:empty_project) }
let(:project_4) { create(:empty_project, :public) }
let(:project_5) { create(:empty_project, namespace: group_1) }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') }
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_5) { create(:label, project: project_5, title: 'Label 5') }
let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
let(:user) { create(:user) }
before do
create(:label, project: project_3, title: 'Label 3')
create(:group_label, group: group_3, title: 'Group Label 4')
project_1.team << [user, :developer]
end
context 'with no filter' do
it 'returns labels from projects the user have access' do
group_2.add_developer(user)
finder = described_class.new(user)
expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
end
end
context 'filtering by group_id' do
it 'returns labels available for any project within the group' do
group_1.add_developer(user)
finder = described_class.new(user, group_id: group_1.id)
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
end
end
context 'filtering by project_id' do
it 'returns labels available for the project' do
finder = described_class.new(user, project_id: project_1.id)
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
end
end
context 'filtering by title' do
it 'returns label with that title' do
finder = described_class.new(user, title: 'Group Label 2')
expect(finder.execute).to eq [group_label_2]
end
end
end
end

View File

@ -13,7 +13,7 @@
"enum": ["backlog", "label", "done"]
},
"label": {
"type": ["object"],
"type": ["object", "null"],
"required": [
"id",
"color",

View File

@ -5,27 +5,26 @@ describe LabelsHelper do
let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) }
context 'with @project set' do
before do
@project = project
end
it 'uses the instance variable' do
expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name%5B%5D=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>}
end
end
context 'without @project set' do
context 'without subject' do
it "uses the label's project" do
expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a project argument' do
let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') }
context 'with a project as subject' do
let(:namespace) { build(:namespace, name: 'foo3') }
let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') }
it 'links to merge requests page' do
expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
it 'links to project issues page' do
expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a group as subject' do
let(:group) { build(:group, name: 'bar') }
it 'links to group issues page' do
expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end

View File

@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
describe 'group label references' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:group_label) { create(:group_label, name: 'gfm references', group: group) }
context 'without project reference' do
let(:reference) { group_label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project)
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}")
expect(reference_filter(act).to_html).to eq exp
end
end
context 'with project reference' do
let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project)
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
expect(reference_filter(act).to_html).to eq exp
end
end
end
describe 'cross project label references' do
context 'valid project referenced' do
let(:another_project) { create(:empty_project, :public) }
@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
end
describe 'cross group label references' do
context 'valid project referenced' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:another_group) { create(:group) }
let(:another_project) { create(:empty_project, :public, namespace: another_group) }
let(:project_name) { another_project.name_with_namespace }
let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
let(:reference) { another_project.to_reference + group_label.to_reference }
let!(:result) { reference_filter("See #{reference}", project: project) }
it 'points to referenced project issues page' do
expect(result.css('a').first.attr('href'))
.to eq urls.namespace_project_issues_url(another_project.namespace,
another_project,
label_name: group_label.name)
end
it 'has valid color' do
expect(result.css('a span').first.attr('style'))
.to match /background-color: #00ff00/
end
it 'contains cross project content' do
expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
end
end
end
end

View File

@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
let(:old_project) { create(:project) }
let(:new_project) { create(:project) }
let(:old_project) { create(:project, name: 'old') }
let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
before { old_project.team << [user, :guest] }
@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
end
context 'description with labels' do
context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
let(:project_ref) { old_project.to_reference }
@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
end
end
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
let(:project_ref) { old_project.to_reference }
before do
old_project.update(namespace: old_group)
end
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
end
end
end
context 'reference contains milestone' do

View File

@ -157,7 +157,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{ type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
]
]
}
described_class.new(project).execute

View File

@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject { described_class.new(project) }
before do
project.team << [project.creator, :master]
project.create_import_data(data: import_data)
end
@ -31,9 +32,9 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject.execute
%w(
Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
).each do |label|
label.sub!("-", ": ")

View File

@ -38,6 +38,7 @@ label:
- label_links
- issues
- merge_requests
- priorities
milestone:
- project
- issues
@ -186,3 +187,5 @@ project:
award_emoji:
- awardable
- user
priorities:
- label

View File

@ -2,6 +2,21 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
"labels": [
{
"id": 2,
"title": "test2",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"type": "ProjectLabel",
"priorities": [
]
}
],
"issues": [
{
"id": 40,
@ -64,7 +79,37 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"priority": null
"type": "ProjectLabel"
}
},
{
"id": 3,
"label_id": 3,
"target_id": 40,
"target_type": "Issue",
"created_at": "2016-07-22T08:57:02.841Z",
"updated_at": "2016-07-22T08:57:02.841Z",
"label": {
"id": 3,
"title": "test3",
"color": "#428bca",
"group_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"project_id": null,
"type": "GroupLabel",
"priorities": [
{
"id": 1,
"project_id": 5,
"label_id": 1,
"priority": 1,
"created_at": "2016-10-18T09:35:43.338Z",
"updated_at": "2016-10-18T09:35:43.338Z"
}
]
}
}
],
@ -536,7 +581,7 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"priority": null
"type": "ProjectLabel"
}
}
],
@ -2226,9 +2271,6 @@
}
]
}
],
"labels": [
],
"milestones": [
{

View File

@ -32,7 +32,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the same label associated to two issues' do
restored_project_json
expect(Label.first.issues.count).to eq(2)
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
@ -107,6 +107,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
it 'has project labels' do
restored_project_json
expect(ProjectLabel.count).to eq(2)
end
it 'has no group labels' do
restored_project_json
expect(GroupLabel.count).to eq(0)
end
context 'with group' do
let!(:project) do
create(:empty_project,
name: 'project',
path: 'project',
builds_access_level: ProjectFeature::DISABLED,
issues_access_level: ProjectFeature::DISABLED,
group: create(:group))
end
it 'has group labels' do
restored_project_json
expect(GroupLabel.count).to eq(1)
end
it 'has label priorities' do
restored_project_json
expect(GroupLabel.first.priorities).not_to be_empty
end
end
it 'has a project feature' do
restored_project_json

Some files were not shown because too many files have changed in this diff Show More