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:
commit
25ff16459c
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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')).
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class GroupLabelPolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.group
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class ProjectLabelPolicy < BasePolicy
|
||||
def rules
|
||||
delegate! @subject.project
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
- if @group.labels.empty?
|
||||
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
- if issue.labels.any?
|
||||
|
||||
- issue.labels.each do |label|
|
||||
= link_to_label(label, project: issue.project)
|
||||
= link_to_label(label, subject: issue.project)
|
||||
- if issue.tasks?
|
||||
|
||||
%span.task-status
|
||||
|
|
|
@ -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');
|
|
@ -1,2 +1,2 @@
|
|||
- if @project.labels.size == 0
|
||||
- if @labels.empty?
|
||||
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
- if merge_request.labels.any?
|
||||
|
||||
- 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?
|
||||
|
||||
%span.task-status
|
||||
|
|
|
@ -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');
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}" }
|
||||
|
|
|
@ -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" } }
|
||||
|
|
|
@ -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'
|
|
@ -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})"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
24
db/schema.rb
24
db/schema.rb
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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+
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
FactoryGirl.define do
|
||||
factory :label_priority do
|
||||
project factory: :empty_project
|
||||
label
|
||||
sequence(:priority)
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -13,7 +13,7 @@
|
|||
"enum": ["backlog", "label", "done"]
|
||||
},
|
||||
"label": {
|
||||
"type": ["object"],
|
||||
"type": ["object", "null"],
|
||||
"required": [
|
||||
"id",
|
||||
"color",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!("-", ": ")
|
||||
|
|
|
@ -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
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue