Merge branch 'master' into 4009-external-users
This commit is contained in:
commit
88f8d3a4d6
|
@ -31,12 +31,14 @@ v 8.6.0 (unreleased)
|
|||
- Add ability to show archived projects on dashboard, explore and group pages
|
||||
- Move group activity to separate page
|
||||
- Create external users which are excluded of internal and private projects unless access was explicitly granted
|
||||
- Continue parameters are checked to ensure redirection goes to the same instance
|
||||
|
||||
v 8.5.5
|
||||
- Ensure removing a project removes associated Todo entries
|
||||
- Prevent a 500 error in Todos when author was removed
|
||||
- Fix pagination for filtered dashboard and explore pages
|
||||
- Fix "Show all" link behavior
|
||||
- Add #upcoming filter to Milestone filter (Tiago Botelho)
|
||||
|
||||
v 8.5.4
|
||||
- Do not cache requests for badges (including builds badge)
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.6.10
|
||||
2.6.11
|
||||
|
|
|
@ -104,6 +104,8 @@ class Dispatcher
|
|||
new ProjectFork()
|
||||
when 'projects:artifacts:browse'
|
||||
new BuildArtifacts()
|
||||
when 'projects:group_links:index'
|
||||
new GroupsSelect()
|
||||
|
||||
switch path.first()
|
||||
when 'admin'
|
||||
|
|
|
@ -238,13 +238,15 @@ class GitLabDropdown
|
|||
selectedObject = @renderedData[selectedIndex]
|
||||
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
|
||||
|
||||
if !value?
|
||||
field.remove()
|
||||
|
||||
if @options.multiSelect
|
||||
oldValue = field.val()
|
||||
if oldValue
|
||||
value = "#{oldValue},#{value}"
|
||||
else
|
||||
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
|
||||
field.remove()
|
||||
|
||||
# Toggle active class for the tick mark
|
||||
el.toggleClass "is-active"
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
module ContinueParams
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def continue_params
|
||||
continue_params = params[:continue]
|
||||
return nil unless continue_params
|
||||
|
||||
continue_params = continue_params.permit(:to, :notice, :notice_now)
|
||||
return unless continue_params[:to] && continue_params[:to].start_with?('/')
|
||||
|
||||
continue_params
|
||||
end
|
||||
end
|
|
@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController
|
|||
@projects = @projects.sort(@sort = params[:sort])
|
||||
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
|
||||
|
||||
@shared_projects = @group.shared_projects
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
||||
|
@ -133,7 +135,7 @@ class GroupsController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def group_params
|
||||
params.require(:group).permit(:name, :description, :path, :avatar, :public)
|
||||
params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
|
||||
end
|
||||
|
||||
def load_events
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Projects::ForksController < Projects::ApplicationController
|
||||
include ContinueParams
|
||||
|
||||
# Authorize
|
||||
before_action :require_non_empty_project
|
||||
before_action :authorize_download_code!
|
||||
|
@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
|
|||
render :error
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def continue_params
|
||||
continue_params = params[:continue]
|
||||
if continue_params
|
||||
continue_params.permit(:to, :notice, :notice_now)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
class Projects::GroupLinksController < Projects::ApplicationController
|
||||
layout 'project_settings'
|
||||
before_action :authorize_admin_project!
|
||||
|
||||
def index
|
||||
@group_links = project.project_group_links.all
|
||||
end
|
||||
|
||||
def create
|
||||
link = project.project_group_links.new
|
||||
link.group_id = params[:link_group_id]
|
||||
link.group_access = params[:link_group_access]
|
||||
link.save
|
||||
|
||||
redirect_to namespace_project_group_links_path(project.namespace, project)
|
||||
end
|
||||
|
||||
def destroy
|
||||
project.project_group_links.find(params[:id]).destroy
|
||||
|
||||
redirect_to namespace_project_group_links_path(project.namespace, project)
|
||||
end
|
||||
end
|
|
@ -1,4 +1,6 @@
|
|||
class Projects::ImportsController < Projects::ApplicationController
|
||||
include ContinueParams
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_project!
|
||||
before_action :require_no_repo, only: [:new, :create]
|
||||
|
@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def continue_params
|
||||
continue_params = params[:continue]
|
||||
|
||||
if continue_params
|
||||
continue_params.permit(:to, :notice, :notice_now)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def finished_notice
|
||||
if @project.forked?
|
||||
'The project was successfully forked.'
|
||||
|
|
|
@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
@project_member = @project.project_members.new
|
||||
@project_group_links = @project.project_group_links
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -244,10 +244,17 @@ class IssuableFinder
|
|||
items
|
||||
end
|
||||
|
||||
def filter_by_upcoming_milestone?
|
||||
params[:milestone_title] == '#upcoming'
|
||||
end
|
||||
|
||||
def by_milestone(items)
|
||||
if milestones?
|
||||
if filter_by_no_milestone?
|
||||
items = items.where(milestone_id: [-1, nil])
|
||||
elsif filter_by_upcoming_milestone?
|
||||
upcoming = Milestone.where(project_id: projects).upcoming
|
||||
items = items.joins(:milestone).where(milestones: { title: upcoming.title })
|
||||
else
|
||||
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
|
||||
|
||||
|
|
|
@ -42,16 +42,14 @@ class ProjectsFinder
|
|||
def group_projects(current_user, group)
|
||||
return [group.projects.public_only] unless current_user
|
||||
|
||||
user_group_projects = [
|
||||
group_projects_for_user(current_user, group),
|
||||
group.shared_projects.visible_to_user(current_user)
|
||||
]
|
||||
if current_user.external?
|
||||
[
|
||||
group_projects_for_user(current_user, group),
|
||||
group.projects.public_only
|
||||
]
|
||||
user_group_projects.push(group.projects.public_only)
|
||||
else
|
||||
[
|
||||
group_projects_for_user(current_user, group),
|
||||
group.projects.public_and_internal_only
|
||||
]
|
||||
user_group_projects.push(group.projects.public_and_internal_only)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ module MilestonesHelper
|
|||
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
|
||||
grouped_milestones.unshift(Milestone::None)
|
||||
grouped_milestones.unshift(Milestone::Any)
|
||||
grouped_milestones.unshift(Milestone::Upcoming)
|
||||
|
||||
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
|
||||
end
|
||||
|
|
|
@ -23,6 +23,8 @@ class Group < Namespace
|
|||
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
|
||||
alias_method :members, :group_members
|
||||
has_many :users, through: :group_members
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
|
||||
|
|
|
@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
|
|||
MilestoneStruct = Struct.new(:title, :name, :id)
|
||||
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
|
||||
Any = MilestoneStruct.new('Any Milestone', '', -1)
|
||||
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
|
||||
|
||||
include InternalId
|
||||
include Sortable
|
||||
|
@ -81,6 +82,10 @@ class Milestone < ActiveRecord::Base
|
|||
super("milestones", /(?<milestone>\d+)/)
|
||||
end
|
||||
|
||||
def self.upcoming
|
||||
self.where('due_date > ?', Time.now).order(due_date: :asc).first
|
||||
end
|
||||
|
||||
def to_reference(from_project = nil)
|
||||
escaped_title = self.title.gsub("]", "\\]")
|
||||
|
||||
|
|
|
@ -173,26 +173,29 @@ class Note < ActiveRecord::Base
|
|||
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
|
||||
end
|
||||
|
||||
# Check if such line of code exists in merge request diff
|
||||
# If exists - its active discussion
|
||||
# If not - its outdated diff
|
||||
# Check if this note is part of an "active" discussion
|
||||
#
|
||||
# This will always return true for anything except MergeRequest noteables,
|
||||
# which have special logic.
|
||||
#
|
||||
# If the note's current diff cannot be matched in the MergeRequest's current
|
||||
# diff, it's considered inactive.
|
||||
def active?
|
||||
return true unless self.diff
|
||||
return false unless noteable
|
||||
return @active if defined?(@active)
|
||||
|
||||
diffs = noteable.diffs(Commit.max_diff_options)
|
||||
notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
|
||||
noteable_diff = find_noteable_diff
|
||||
|
||||
return @active = false if notable_diff.nil?
|
||||
if noteable_diff
|
||||
parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
|
||||
|
||||
parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
|
||||
# We cannot use ||= because @active may be false
|
||||
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
|
||||
end
|
||||
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
|
||||
else
|
||||
@active = false
|
||||
end
|
||||
|
||||
def outdated?
|
||||
!active?
|
||||
@active
|
||||
end
|
||||
|
||||
def diff_file_index
|
||||
|
@ -380,6 +383,12 @@ class Note < ActiveRecord::Base
|
|||
self.line_code = nil if self.line_code.blank?
|
||||
end
|
||||
|
||||
# Find the diff on noteable that matches our own
|
||||
def find_noteable_diff
|
||||
diffs = noteable.diffs(Commit.max_diff_options)
|
||||
diffs.find { |d| d.new_path == self.diff.new_path }
|
||||
end
|
||||
|
||||
def awards_supported?
|
||||
(for_issue? || for_merge_request?) && !for_diff_line?
|
||||
end
|
||||
|
|
|
@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
|
|||
has_many :releases, dependent: :destroy
|
||||
has_many :lfs_objects_projects, dependent: :destroy
|
||||
has_many :lfs_objects, through: :lfs_objects_projects
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :invited_groups, through: :project_group_links, source: :group
|
||||
has_many :todos, dependent: :destroy
|
||||
|
||||
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
|
||||
|
@ -893,6 +895,10 @@ class Project < ActiveRecord::Base
|
|||
jira_tracker? && jira_service.active
|
||||
end
|
||||
|
||||
def allowed_to_share_with_group?
|
||||
!namespace.share_with_group_lock
|
||||
end
|
||||
|
||||
def ci_commit(sha)
|
||||
ci_commits.find_by(sha: sha)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
class ProjectGroupLink < ActiveRecord::Base
|
||||
GUEST = 10
|
||||
REPORTER = 20
|
||||
DEVELOPER = 30
|
||||
MASTER = 40
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :group
|
||||
|
||||
validates :project_id, presence: true
|
||||
validates :group_id, presence: true
|
||||
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
|
||||
validates :group_access, presence: true
|
||||
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
|
||||
validate :different_group
|
||||
|
||||
def self.access_options
|
||||
Gitlab::Access.options
|
||||
end
|
||||
|
||||
def self.default_access
|
||||
DEVELOPER
|
||||
end
|
||||
|
||||
def human_access
|
||||
self.class.access_options.key(self.group_access)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def different_group
|
||||
if self.group && self.project && self.project.group == self.group
|
||||
errors.add(:base, "Project cannot be shared with the project it is in.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -160,7 +160,27 @@ class ProjectTeam
|
|||
end
|
||||
end
|
||||
|
||||
access.max
|
||||
if project.invited_groups.any? && project.allowed_to_share_with_group?
|
||||
access << max_invited_level(user_id)
|
||||
end
|
||||
|
||||
access.compact.max
|
||||
end
|
||||
|
||||
|
||||
def max_invited_level(user_id)
|
||||
project.project_group_links.map do |group_link|
|
||||
invited_group = group_link.group
|
||||
access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
|
||||
|
||||
# If group member has higher access level we should restrict it
|
||||
# to max allowed access level
|
||||
if access && access > group_link.group_access
|
||||
access = group_link.group_access
|
||||
end
|
||||
|
||||
access
|
||||
end.compact.max
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -168,6 +188,35 @@ class ProjectTeam
|
|||
def fetch_members(level = nil)
|
||||
project_members = project.project_members
|
||||
group_members = group ? group.group_members : []
|
||||
invited_members = []
|
||||
|
||||
if project.invited_groups.any? && project.allowed_to_share_with_group?
|
||||
project.project_group_links.each do |group_link|
|
||||
invited_group = group_link.group
|
||||
im = invited_group.group_members
|
||||
|
||||
if level
|
||||
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
|
||||
|
||||
# Skip group members if we ask for masters
|
||||
# but max group access is developers
|
||||
next if int_level > group_link.group_access
|
||||
|
||||
# If we ask for developers and max
|
||||
# group access is developers we need to provide
|
||||
# both group master, developers as devs
|
||||
if int_level == group_link.group_access
|
||||
im.where("access_level >= ?)", group_link.group_access)
|
||||
else
|
||||
im.send(level)
|
||||
end
|
||||
end
|
||||
|
||||
invited_members << im
|
||||
end
|
||||
|
||||
invited_members = invited_members.flatten.compact
|
||||
end
|
||||
|
||||
if level
|
||||
project_members = project_members.send(level)
|
||||
|
@ -175,6 +224,7 @@ class ProjectTeam
|
|||
end
|
||||
|
||||
user_ids = project_members.pluck(:user_id)
|
||||
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
|
||||
user_ids.push(*group_members.pluck(:user_id)) if group
|
||||
|
||||
User.where(id: user_ids)
|
||||
|
|
|
@ -838,7 +838,8 @@ class User < ActiveRecord::Base
|
|||
def projects_union
|
||||
Gitlab::SQL::Union.new([personal_projects.select(:id),
|
||||
groups_projects.select(:id),
|
||||
projects.select(:id)])
|
||||
projects.select(:id),
|
||||
groups.joins(:shared_projects).select(:project_id)])
|
||||
end
|
||||
|
||||
def ci_projects_union
|
||||
|
|
|
@ -50,6 +50,22 @@
|
|||
.panel-footer
|
||||
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
|
||||
|
||||
- if @group.shared_projects.any?
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Projects shared with #{@group.name}
|
||||
%span.badge
|
||||
#{@group.shared_projects.count}
|
||||
%ul.well-list
|
||||
- @group.shared_projects.sort_by(&:name).each do |project|
|
||||
%li
|
||||
%strong
|
||||
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
|
||||
%span.label.label-gray
|
||||
= repository_size(project)
|
||||
%span.pull-right.light
|
||||
%span.monospace= project.path_with_namespace + ".git"
|
||||
|
||||
.col-md-6
|
||||
- if can?(current_user, :admin_group_member, @group)
|
||||
.panel.panel-default
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
- if projects.present?
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Projects shared with
|
||||
%strong #{@group.name}
|
||||
(#{projects.count})
|
||||
%ul.well-list
|
||||
- projects.each do |project|
|
||||
%li.project-row
|
||||
= link_to namespace_project_path(project.namespace, project), class: dom_class(project) do
|
||||
%span.namespace-name
|
||||
- if project.namespace
|
||||
= project.namespace.human_name
|
||||
\/
|
||||
%span.project-name
|
||||
= truncate(project.name, length: 25)
|
||||
%span.arrow
|
||||
%i.icon-angle-right
|
|
@ -23,6 +23,15 @@
|
|||
%hr
|
||||
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
|
||||
|
||||
.form-group
|
||||
%hr
|
||||
= f.label :share_with_group_lock, class: 'control-label' do
|
||||
Share with group lock
|
||||
.col-sm-10
|
||||
.checkbox
|
||||
= f.check_box :share_with_group_lock
|
||||
%span.descr Prevent sharing a project with another group within this group
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Save group', class: "btn btn-save"
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
%li.active
|
||||
= link_to "#projects", 'data-toggle' => 'tab' do
|
||||
Projects
|
||||
- if @shared_projects.present?
|
||||
%li
|
||||
= link_to "#shared", 'data-toggle' => 'tab' do
|
||||
Shared Projects
|
||||
|
||||
- if can?(current_user, :read_group, @group)
|
||||
%div{ class: container_class }
|
||||
|
@ -39,6 +43,9 @@
|
|||
.tab-pane.active#projects
|
||||
= render "projects", projects: @projects
|
||||
|
||||
.tab-pane#shared
|
||||
= render "shared_projects", projects: @shared_projects
|
||||
|
||||
- else
|
||||
%p.nav-links.no-top
|
||||
No projects to show
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
|
||||
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
|
||||
= icon('home fw')
|
||||
= icon('bookmark fw')
|
||||
%span
|
||||
Project
|
||||
= nav_link(path: 'projects#activity') do
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
= icon('pencil-square-o fw')
|
||||
%span
|
||||
Project Settings
|
||||
- if @project.allowed_to_share_with_group?
|
||||
= nav_link(controller: :group_links) do
|
||||
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
|
||||
= icon('share-square-o fw')
|
||||
%span
|
||||
Groups
|
||||
= nav_link(controller: :deploy_keys) do
|
||||
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
|
||||
= icon('key fw')
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
- page_title "Groups"
|
||||
%h3.page_title Share project with other groups
|
||||
%p.light
|
||||
Projects can be stored in only one group at once. However you can share a project with other groups here.
|
||||
%hr
|
||||
- if @group_links.present?
|
||||
.enabled-groups.panel.panel-default
|
||||
.panel-heading
|
||||
Already shared with
|
||||
%ul.well-list
|
||||
- @group_links.each do |group_link|
|
||||
- group = group_link.group
|
||||
%li
|
||||
.pull-right
|
||||
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
|
||||
%i.icon-remove
|
||||
disable sharing
|
||||
= link_to group do
|
||||
%strong
|
||||
%i.icon-folder-open
|
||||
= group.name
|
||||
%br
|
||||
.light up to #{group_link.human_access}
|
||||
|
||||
|
||||
.available-groups
|
||||
%h4
|
||||
Can be shared with
|
||||
%div
|
||||
= form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
|
||||
.form-group
|
||||
= label_tag :link_group_id, 'Group', class: 'control-label'
|
||||
.col-sm-10
|
||||
= groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
|
||||
.form-group
|
||||
= label_tag :link_group_access, 'Max access level', class: 'control-label'
|
||||
.col-sm-10
|
||||
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
|
||||
.form-actions
|
||||
= submit_tag "Share", class: "btn btn-create"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
- @project_group_links.each do |group_links|
|
||||
- shared_group = group_links.group
|
||||
- shared_group_users_count = group_links.group.group_members.count
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Shared with
|
||||
%strong #{shared_group.name}
|
||||
group, members with
|
||||
%strong #{group_links.human_access}
|
||||
role (#{shared_group_users_count})
|
||||
- if current_user.can?(:admin_group, shared_group)
|
||||
.panel-head-actions
|
||||
= link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit group members
|
||||
%ul.content-list
|
||||
- shared_group.group_members.order('access_level DESC').limit(20).each do |member|
|
||||
= render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
|
||||
- if shared_group_users_count > 20
|
||||
%li
|
||||
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
|
|
@ -18,3 +18,6 @@
|
|||
|
||||
- if @group
|
||||
= render "group_members", members: @group_members
|
||||
|
||||
- if @project_group_links.any? && @project.allowed_to_share_with_group?
|
||||
= render "shared_group_members"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
.filter-item.inline
|
||||
- if params[:assignee_id]
|
||||
= hidden_field_tag(:assignee_id, params[:assignee_id])
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
|
||||
placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
|
||||
|
||||
.filter-item.inline.milestone-filter
|
||||
|
|
|
@ -701,6 +701,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
|
||||
|
||||
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
|
||||
member do
|
||||
delete :delete_attachment
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
class CreateProjectGroupLinks < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :project_group_links do |t|
|
||||
t.integer :project_id, null: false
|
||||
t.integer :group_id, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddAccessToProjectGroupLink < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddGroupShareLock < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :namespaces, :share_with_group_lock, :boolean, default: false
|
||||
end
|
||||
end
|
21
db/schema.rb
21
db/schema.rb
|
@ -578,14 +578,15 @@ ActiveRecord::Schema.define(version: 20160310185910) do
|
|||
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
|
||||
|
||||
create_table "namespaces", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "path", null: false
|
||||
t.string "name", null: false
|
||||
t.string "path", null: false
|
||||
t.integer "owner_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "type"
|
||||
t.string "description", default: "", null: false
|
||||
t.string "description", default: "", null: false
|
||||
t.string "avatar"
|
||||
t.boolean "share_with_group_lock", default: false
|
||||
end
|
||||
|
||||
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
|
||||
|
@ -669,6 +670,14 @@ ActiveRecord::Schema.define(version: 20160310185910) do
|
|||
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
|
||||
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
|
||||
|
||||
create_table "project_group_links", force: :cascade do |t|
|
||||
t.integer "project_id", null: false
|
||||
t.integer "group_id", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "group_access", default: 30, null: false
|
||||
end
|
||||
|
||||
create_table "project_import_data", force: :cascade do |t|
|
||||
t.integer "project_id"
|
||||
t.text "data"
|
||||
|
@ -765,9 +774,9 @@ ActiveRecord::Schema.define(version: 20160310185910) do
|
|||
t.string "type"
|
||||
t.string "title"
|
||||
t.integer "project_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "active", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.boolean "active", default: false, null: false
|
||||
t.text "properties"
|
||||
t.boolean "template", default: false
|
||||
t.boolean "push_events", default: true
|
||||
|
|
|
@ -145,6 +145,7 @@ Parameters:
|
|||
"state": "active",
|
||||
"created_at": "2013-09-30T13:46:01Z"
|
||||
},
|
||||
"expires_at": null,
|
||||
"updated_at": "2013-10-02T07:34:20Z",
|
||||
"created_at": "2013-10-02T07:34:20Z"
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ Parameters:
|
|||
"state": "active",
|
||||
"created_at": "2012-05-23T08:00:58Z"
|
||||
},
|
||||
"expires_at": null,
|
||||
"updated_at": "2012-06-28T10:52:04Z",
|
||||
"created_at": "2012-06-28T10:52:04Z"
|
||||
}
|
||||
|
|
|
@ -619,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid
|
|||
Please note that the returned JSON currently differs slightly. Thus you should not
|
||||
rely on the returned JSON structure.
|
||||
|
||||
### Share project with group
|
||||
|
||||
Allow to share project with group.
|
||||
|
||||
```
|
||||
POST /projects/:id/share
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
- `id` (required) - The ID of a project
|
||||
- `group_id` (required) - The ID of a group
|
||||
- `group_access` (required) - Level of permissions for sharing
|
||||
|
||||
## Hooks
|
||||
|
||||
Also called Project Hooks and Webhooks.
|
||||
|
|
|
@ -233,9 +233,9 @@ sudo usermod -aG redis git
|
|||
### Clone the Source
|
||||
|
||||
# Clone GitLab repository
|
||||
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-5-stable gitlab
|
||||
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
|
||||
|
||||
**Note:** You can change `8-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
|
||||
**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
|
||||
|
||||
### Configure It
|
||||
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
# From 8.5 to 8.6
|
||||
|
||||
### 1. Stop server
|
||||
|
||||
sudo service gitlab stop
|
||||
|
||||
### 2. Backup
|
||||
|
||||
```bash
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
|
||||
```
|
||||
|
||||
### 3. Get latest code
|
||||
|
||||
```bash
|
||||
sudo -u git -H git fetch --all
|
||||
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
|
||||
```
|
||||
|
||||
For GitLab Community Edition:
|
||||
|
||||
```bash
|
||||
sudo -u git -H git checkout 8-6-stable
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
For GitLab Enterprise Edition:
|
||||
|
||||
```bash
|
||||
sudo -u git -H git checkout 8-6-stable-ee
|
||||
```
|
||||
|
||||
### 4. Update gitlab-shell
|
||||
|
||||
```bash
|
||||
cd /home/git/gitlab-shell
|
||||
sudo -u git -H git fetch --all
|
||||
sudo -u git -H git checkout v2.6.11
|
||||
```
|
||||
|
||||
### 5. Update gitlab-workhorse
|
||||
|
||||
Install and compile gitlab-workhorse. This requires
|
||||
[Go 1.5](https://golang.org/dl) which should already be on your system from
|
||||
GitLab 8.1.
|
||||
|
||||
```bash
|
||||
cd /home/git/gitlab-workhorse
|
||||
sudo -u git -H git fetch --all
|
||||
sudo -u git -H git checkout 0.6.5
|
||||
sudo -u git -H make
|
||||
```
|
||||
|
||||
### 6. Install libs, migrations, etc.
|
||||
|
||||
```bash
|
||||
cd /home/git/gitlab
|
||||
|
||||
# MySQL installations (note: the line below states '--without postgres')
|
||||
sudo -u git -H bundle install --without postgres development test --deployment
|
||||
|
||||
# PostgreSQL installations (note: the line below states '--without mysql')
|
||||
sudo -u git -H bundle install --without mysql development test --deployment
|
||||
|
||||
# Optional: clean up old gems
|
||||
sudo -u git -H bundle clean
|
||||
|
||||
# Run database migrations
|
||||
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
|
||||
|
||||
# Clean up assets and cache
|
||||
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
|
||||
|
||||
```
|
||||
|
||||
### 7. Update configuration files
|
||||
|
||||
#### New configuration options for `gitlab.yml`
|
||||
|
||||
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
|
||||
|
||||
```sh
|
||||
git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
|
||||
```
|
||||
|
||||
#### Nginx configuration
|
||||
|
||||
Ensure you're still up-to-date with the latest NGINX configuration changes:
|
||||
|
||||
```sh
|
||||
# For HTTPS configurations
|
||||
git diff origin/8-5-stable:lib/support/nginx/gitlab-ssl origin/8-6-stable:lib/support/nginx/gitlab-ssl
|
||||
|
||||
# For HTTP configurations
|
||||
git diff origin/8-5-stable:lib/support/nginx/gitlab origin/8-6-stable:lib/support/nginx/gitlab
|
||||
```
|
||||
|
||||
If you are using Apache instead of NGINX please see the updated [Apache templates].
|
||||
Also note that because Apache does not support upstreams behind Unix sockets you
|
||||
will need to let gitlab-workhorse listen on a TCP port. You can do this
|
||||
via [/etc/default/gitlab].
|
||||
|
||||
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
|
||||
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-6-stable/lib/support/init.d/gitlab.default.example#L37
|
||||
|
||||
#### Init script
|
||||
|
||||
Ensure you're still up-to-date with the latest init script changes:
|
||||
|
||||
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
|
||||
|
||||
### 8. Updates for PostgreSQL Users
|
||||
|
||||
Starting with 8.6 users using GitLab in combination with PostgreSQL are required
|
||||
to have the `pg_trgm` extension enabled for all GitLab databases. If you're
|
||||
using GitLab's Omnibus packages there's nothing you'll need to do manually as
|
||||
this extension is enabled automatically. Users who install GitLab without using
|
||||
Omnibus (e.g. by building from source) have to enable this extension manually.
|
||||
To enable this extension run the following SQL command as a PostgreSQL super
|
||||
user for _every_ GitLab database:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
```
|
||||
|
||||
Certain operating systems might require the installation of extra packages for
|
||||
this extension to be available. For example, users using Ubuntu will have to
|
||||
install the `postgresql-contrib` package in order for this extension to be
|
||||
available.
|
||||
|
||||
### 9. Start application
|
||||
|
||||
sudo service gitlab start
|
||||
sudo service nginx restart
|
||||
|
||||
### 10. Check application status
|
||||
|
||||
Check if GitLab and its environment are configured correctly:
|
||||
|
||||
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
|
||||
|
||||
To make sure you didn't miss anything run a more thorough check:
|
||||
|
||||
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
|
||||
|
||||
If all items are green, then congratulations, the upgrade is complete!
|
||||
|
||||
## Things went south? Revert to previous version (8.5)
|
||||
|
||||
### 1. Revert the code to the previous version
|
||||
|
||||
Follow the [upgrade guide from 8.4 to 8.5](8.4-to-8.5.md), except for the
|
||||
database migration (the backup is already migrated to the previous version).
|
||||
|
||||
### 2. Restore from the backup
|
||||
|
||||
```bash
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
|
||||
```
|
||||
|
||||
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
|
|
@ -582,6 +582,7 @@ X-Gitlab-Event: Note Hook
|
|||
"created_at": "2015-04-09 02:40:38 UTC",
|
||||
"updated_at": "2015-04-09 02:40:38 UTC",
|
||||
"file_name": "test.rb",
|
||||
"expires_at": null,
|
||||
"type": "ProjectSnippet",
|
||||
"visibility_level": 0
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
- [Project forking workflow](forking_workflow.md)
|
||||
- [Project users](add-user/add-user.md)
|
||||
- [Protected branches](protected_branches.md)
|
||||
- [Sharing a project with a group](share_with_group.md)
|
||||
- [Share projects with other groups](share_projects_with_other_groups.md)
|
||||
- [Web Editor](web_editor.md)
|
||||
- [Releases](releases.md)
|
||||
- [Milestones](milestones.md)
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
|
@ -0,0 +1,30 @@
|
|||
# Share Projects with other Groups
|
||||
|
||||
In GitLab Enterprise Edition you can share projects with other groups.
|
||||
This makes it possible to add a group of users to a project with a single action.
|
||||
|
||||
## Groups as collections of users
|
||||
|
||||
In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
|
||||
In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
|
||||
|
||||
## Sharing a project with a group of users
|
||||
|
||||
The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
|
||||
But what if 'Project Acme' already belongs to another group, say 'Open Source'?
|
||||
This is where the (Enterprise Edition only) group sharing feature can be of use.
|
||||
|
||||
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
|
||||
|
||||
![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
|
||||
|
||||
Now you can add the 'Engineering' group with the maximum access level of your choice.
|
||||
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
|
||||
|
||||
!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
|
||||
|
||||
## Maximum access level
|
||||
|
||||
!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
|
||||
|
||||
In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
|
|
@ -0,0 +1,13 @@
|
|||
# Sharing a project with a group
|
||||
|
||||
If you want to share a single project in a group with another group,
|
||||
you can do so easily. By setting the permission you can quickly
|
||||
give a select group of users access to a project in a restricted manner.
|
||||
|
||||
In a project go to the project settings -> groups.
|
||||
|
||||
Now you can select a group that you want to share this project with and with
|
||||
which maximum access level. Users in that group are able to access this project
|
||||
with their set group access level, up to the maximum level that you've set.
|
||||
|
||||
![Share a project with a group](share_with_group.png)
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
|
@ -21,6 +21,11 @@ Feature: Admin Groups
|
|||
When I select user "John Doe" from user list as "Reporter"
|
||||
Then I should see "John Doe" in team list in every project as "Reporter"
|
||||
|
||||
Scenario: Shared projects
|
||||
Given group has shared projects
|
||||
When I visit group page
|
||||
Then I should see project shared with group
|
||||
|
||||
@javascript
|
||||
Scenario: Remove user from group
|
||||
Given we have user "John Doe" in group
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
Feature: Project Group Links
|
||||
Background:
|
||||
Given I sign in as a user
|
||||
And I own project "Shop"
|
||||
And project "Shop" is shared with group "Ops"
|
||||
And project "Shop" is not shared with group "Market"
|
||||
And I visit project group links page
|
||||
|
||||
Scenario: I should see list of groups
|
||||
Then I should see project already shared with group "Ops"
|
||||
Then I should see project is not shared with group "Market"
|
||||
|
||||
@javascript
|
||||
Scenario: I share project with group
|
||||
When I select group "Market" for share
|
||||
Then I should see project is shared with group "Market"
|
|
@ -34,9 +34,10 @@ Feature: Project Network Graph
|
|||
@javascript
|
||||
Scenario: I should filter selected tag
|
||||
When I switch ref to "v1.0.0"
|
||||
Then page should have "v1.0.0" in title
|
||||
Then page should have content not containing "v1.0.0"
|
||||
When click "Show only selected branch" checkbox
|
||||
Then page should not have content not containing "v1.0.0"
|
||||
Then page should only have content from "v1.0.0"
|
||||
When click "Show only selected branch" checkbox
|
||||
Then page should have content not containing "v1.0.0"
|
||||
|
||||
|
|
|
@ -39,3 +39,8 @@ Feature: Project Team Management
|
|||
And I click link "Import team from another project"
|
||||
And I submit "Website" project for import team
|
||||
Then I should see "Mike" in team list as "Reporter"
|
||||
|
||||
Scenario: See all members of projects shared group
|
||||
Given I share project with group "OpenSource"
|
||||
And I visit project "Shop" team page
|
||||
Then I should see "Opensource" group user listing
|
||||
|
|
|
@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
|
|||
end
|
||||
end
|
||||
|
||||
step 'group has shared projects' do
|
||||
share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
|
||||
share_link.group_id = current_group.id
|
||||
share_link.save!
|
||||
end
|
||||
|
||||
step 'I visit group page' do
|
||||
visit admin_group_path(current_group)
|
||||
end
|
||||
|
||||
step 'I should see project shared with group' do
|
||||
expect(page).to have_content(shared_project.name_with_namespace)
|
||||
expect(page).to have_content "Projects shared with"
|
||||
end
|
||||
|
||||
step 'we have user "John Doe" in group' do
|
||||
current_group.add_reporter(user_john)
|
||||
end
|
||||
|
@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
|
|||
@group ||= Group.first
|
||||
end
|
||||
|
||||
def shared_project
|
||||
@shared_project ||= create(:empty_project)
|
||||
end
|
||||
|
||||
def user_john
|
||||
@user_john ||= User.find_by(name: "John Doe")
|
||||
end
|
||||
|
|
|
@ -36,22 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click "Authored by me" link' do
|
||||
execute_script('$("#assignee_id").val("")')
|
||||
execute_script('$(".js-user-search").first().click()')
|
||||
sleep 1
|
||||
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
|
||||
sleep 1
|
||||
find("#assignee_id").set("")
|
||||
find(".js-author-search", match: :first).click
|
||||
find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
|
||||
end
|
||||
|
||||
step 'I click "All" link' do
|
||||
execute_script('$(".js-user-search").first().click()')
|
||||
sleep 1
|
||||
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
|
||||
sleep 1
|
||||
execute_script('$(".js-user-search").eq(1).click()')
|
||||
sleep 1
|
||||
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
|
||||
sleep 1
|
||||
find('.js-author-search').click
|
||||
find('.dropdown-menu-user-full-name', match: :first).click
|
||||
|
||||
find('.js-assignee-search').click
|
||||
find('.dropdown-menu-user-full-name', match: :first).click
|
||||
end
|
||||
|
||||
def should_see(issue)
|
||||
|
|
|
@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click "Authored by me" link' do
|
||||
execute_script('$("#assignee_id").val("")')
|
||||
execute_script('$(".js-user-search").first().click()')
|
||||
sleep 0.5
|
||||
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
|
||||
sleep 2
|
||||
find("#assignee_id").set("")
|
||||
find(".js-author-search", match: :first).click
|
||||
find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
|
||||
end
|
||||
|
||||
step 'I click "All" link' do
|
||||
execute_script('$(".js-user-search").first().click()')
|
||||
sleep 0.5
|
||||
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
|
||||
sleep 2
|
||||
execute_script('$(".js-user-search").eq(1).click()')
|
||||
sleep 0.5
|
||||
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
|
||||
sleep 2
|
||||
find(".js-author-search").click
|
||||
find(".dropdown-menu-author li a", match: :first).click
|
||||
find(".js-assignee-search").click
|
||||
find(".dropdown-menu-assignee li a", match: :first).click
|
||||
end
|
||||
|
||||
def should_see(merge_request)
|
||||
|
|
|
@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
|
|||
|
||||
When 'I switch ref to "feature"' do
|
||||
select 'feature', from: 'ref'
|
||||
sleep 2
|
||||
end
|
||||
|
||||
When 'I switch ref to "v1.0.0"' do
|
||||
select 'v1.0.0', from: 'ref'
|
||||
sleep 2
|
||||
end
|
||||
|
||||
When 'click "Show only selected branch" checkbox' do
|
||||
find('#filter_ref').click
|
||||
sleep 2
|
||||
end
|
||||
|
||||
step 'page should have content not containing "v1.0.0"' do
|
||||
|
@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
|
|||
end
|
||||
end
|
||||
|
||||
step 'page should not have content not containing "v1.0.0"' do
|
||||
step 'page should have "v1.0.0" in title' do
|
||||
expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
|
||||
end
|
||||
|
||||
step 'page should only have content from "v1.0.0"' do
|
||||
page.within '.network-graph' do
|
||||
expect(page).not_to have_content 'Change some files'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
|
||||
include SharedAuthentication
|
||||
include SharedProject
|
||||
include SharedPaths
|
||||
include Select2Helper
|
||||
|
||||
step 'I should see project already shared with group "Ops"' do
|
||||
page.within '.enabled-groups' do
|
||||
expect(page).to have_content "Ops"
|
||||
end
|
||||
end
|
||||
|
||||
step 'I should see project is not shared with group "Market"' do
|
||||
page.within '.enabled-groups' do
|
||||
expect(page).not_to have_content "Market"
|
||||
end
|
||||
end
|
||||
|
||||
step 'I select group "Market" for share' do
|
||||
group = Group.find_by(path: 'market')
|
||||
select2(group.id, from: "#link_group_id")
|
||||
select "Master", from: 'link_group_access'
|
||||
click_button "Share"
|
||||
end
|
||||
|
||||
step 'I should see project is shared with group "Market"' do
|
||||
page.within '.enabled-groups' do
|
||||
expect(page).to have_content "Market"
|
||||
end
|
||||
end
|
||||
|
||||
step 'project "Shop" is shared with group "Ops"' do
|
||||
group = create(:group, name: 'Ops')
|
||||
share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
|
||||
share_link.group_id = group.id
|
||||
share_link.save!
|
||||
end
|
||||
|
||||
step 'project "Shop" is not shared with group "Market"' do
|
||||
create(:group, name: 'Market', path: 'market')
|
||||
end
|
||||
|
||||
step 'I visit project group links page' do
|
||||
visit namespace_project_group_links_path(project.namespace, project)
|
||||
end
|
||||
|
||||
def project
|
||||
@project ||= Project.find_by_name "Shop"
|
||||
end
|
||||
end
|
|
@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
|
|||
click_link('Remove user from team')
|
||||
end
|
||||
end
|
||||
|
||||
step 'I share project with group "OpenSource"' do
|
||||
project = Project.find_by(name: 'Shop')
|
||||
os_group = create(:group, name: 'OpenSource')
|
||||
create(:project, group: os_group)
|
||||
@os_user1 = create(:user)
|
||||
@os_user2 = create(:user)
|
||||
os_group.add_owner(@os_user1)
|
||||
os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
|
||||
share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
|
||||
share_link.group_id = os_group.id
|
||||
share_link.save!
|
||||
end
|
||||
|
||||
step 'I should see "Opensource" group user listing' do
|
||||
expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
|
||||
expect(page).to have_content(@os_user1.name)
|
||||
expect(page).to have_content(@os_user2.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -144,6 +144,9 @@ module API
|
|||
expose :id, :title, :file_name
|
||||
expose :author, using: Entities::UserBasic
|
||||
expose :updated_at, :created_at
|
||||
|
||||
# TODO (rspeicher): Deprecated; remove in 9.0
|
||||
expose(:expires_at) { |snippet| nil }
|
||||
end
|
||||
|
||||
class ProjectEntity < Grape::Entity
|
||||
|
@ -243,6 +246,10 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
class ProjectGroupLink < Grape::Entity
|
||||
expose :id, :project_id, :group_id, :group_access
|
||||
end
|
||||
|
||||
class Namespace < Grape::Entity
|
||||
expose :id, :path, :kind
|
||||
end
|
||||
|
|
|
@ -290,6 +290,33 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# Share project with group
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# group_id (required) - The ID of a group
|
||||
# group_access (required) - Level of permissions for sharing
|
||||
#
|
||||
# Example Request:
|
||||
# POST /projects/:id/share
|
||||
post ":id/share" do
|
||||
authorize! :admin_project, user_project
|
||||
required_attributes! [:group_id, :group_access]
|
||||
|
||||
unless user_project.allowed_to_share_with_group?
|
||||
return render_api_error!("The project sharing with group is disabled", 400)
|
||||
end
|
||||
|
||||
link = user_project.project_group_links.new
|
||||
link.group_id = params[:group_id]
|
||||
link.group_access = params[:group_access]
|
||||
if link.save
|
||||
present link, with: Entities::ProjectGroupLink
|
||||
else
|
||||
render_api_error!(link.errors.full_messages.first, 409)
|
||||
end
|
||||
end
|
||||
|
||||
# Upload a file
|
||||
#
|
||||
# Parameters:
|
||||
|
|
|
@ -19,7 +19,7 @@ describe Projects::ImportsController do
|
|||
end
|
||||
|
||||
it 'sets flash.now if params is present' do
|
||||
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'Started' }
|
||||
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
|
||||
|
||||
expect(flash.now[:notice]).to eq 'Started'
|
||||
end
|
||||
|
@ -45,7 +45,7 @@ describe Projects::ImportsController do
|
|||
end
|
||||
|
||||
it 'sets flash.now if params is present' do
|
||||
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'In progress' }
|
||||
get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
|
||||
|
||||
expect(flash.now[:notice]).to eq 'In progress'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
FactoryGirl.define do
|
||||
factory :project_group_link do
|
||||
project
|
||||
group
|
||||
end
|
||||
end
|
|
@ -17,6 +17,10 @@ describe ProjectsFinder do
|
|||
create(:project, :public, group: group, name: 'C', path: 'C')
|
||||
end
|
||||
|
||||
let!(:shared_project) do
|
||||
create(:project, :private, name: 'D', path: 'D')
|
||||
end
|
||||
|
||||
let(:finder) { described_class.new }
|
||||
|
||||
describe 'without a group' do
|
||||
|
@ -56,7 +60,35 @@ describe ProjectsFinder do
|
|||
describe 'with a user' do
|
||||
subject { finder.execute(user, group: group) }
|
||||
|
||||
it { is_expected.to eq([public_project, internal_project]) }
|
||||
describe 'without shared projects' do
|
||||
it { is_expected.to eq([public_project, internal_project]) }
|
||||
end
|
||||
|
||||
describe 'with shared projects and group membership' do
|
||||
before do
|
||||
group.add_user(user, Gitlab::Access::DEVELOPER)
|
||||
|
||||
shared_project.project_group_links.
|
||||
create(group_access: Gitlab::Access::MASTER, group: group)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([shared_project, public_project, internal_project])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with shared projects and project membership' do
|
||||
before do
|
||||
shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
|
||||
|
||||
shared_project.project_group_links.
|
||||
create(group_access: Gitlab::Access::MASTER, group: group)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([shared_project, public_project, internal_project])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -152,7 +152,7 @@ describe Note, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe :grouped_awards do
|
||||
describe '.grouped_awards' do
|
||||
before do
|
||||
create :note, note: "smile", is_award: true
|
||||
create :note, note: "smile", is_award: true
|
||||
|
@ -169,6 +169,66 @@ describe Note, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'is always true when the note has no associated diff' do
|
||||
note = build(:note)
|
||||
|
||||
expect(note).to receive(:diff).and_return(nil)
|
||||
|
||||
expect(note).to be_active
|
||||
end
|
||||
|
||||
it 'is never true when the note has no noteable associated' do
|
||||
note = build(:note)
|
||||
|
||||
expect(note).to receive(:diff).and_return(double)
|
||||
expect(note).to receive(:noteable).and_return(nil)
|
||||
|
||||
expect(note).not_to be_active
|
||||
end
|
||||
|
||||
it 'returns the memoized value if defined' do
|
||||
note = build(:note)
|
||||
|
||||
expect(note).to receive(:diff).and_return(double)
|
||||
expect(note).to receive(:noteable).and_return(double)
|
||||
|
||||
note.instance_variable_set(:@active, 'foo')
|
||||
expect(note).not_to receive(:find_noteable_diff)
|
||||
|
||||
expect(note.active?).to eq 'foo'
|
||||
end
|
||||
|
||||
context 'for a merge request noteable' do
|
||||
it 'is false when noteable has no matching diff' do
|
||||
merge = build_stubbed(:merge_request, :simple)
|
||||
note = build(:note, noteable: merge)
|
||||
|
||||
allow(note).to receive(:diff).and_return(double)
|
||||
expect(note).to receive(:find_noteable_diff).and_return(nil)
|
||||
|
||||
expect(note).not_to be_active
|
||||
end
|
||||
|
||||
it 'is true when noteable has a matching diff' do
|
||||
merge = create(:merge_request, :simple)
|
||||
|
||||
# Generate a real line_code value so we know it will match. We use a
|
||||
# random line from a random diff just for funsies.
|
||||
diff = merge.diffs.to_a.sample
|
||||
line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
|
||||
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
|
||||
|
||||
# We're persisting in order to trigger the set_diff callback
|
||||
note = create(:note, noteable: merge, line_code: code)
|
||||
|
||||
# Make sure we don't get a false positive from a guard clause
|
||||
expect(note).to receive(:find_noteable_diff).and_call_original
|
||||
expect(note).to be_active
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "editable?" do
|
||||
it "returns true" do
|
||||
note = build(:note)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ProjectGroupLink do
|
||||
describe "Associations" do
|
||||
it { should belong_to(:group) }
|
||||
it { should belong_to(:project) }
|
||||
end
|
||||
|
||||
describe "Validation" do
|
||||
let!(:project_group_link) { create(:project_group_link) }
|
||||
|
||||
it { should validate_presence_of(:project_id) }
|
||||
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
|
||||
it { should validate_presence_of(:group_id) }
|
||||
it { should validate_presence_of(:group_access) }
|
||||
end
|
||||
end
|
|
@ -67,6 +67,50 @@ describe ProjectTeam, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe :max_invited_level do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:empty_project) }
|
||||
|
||||
before do
|
||||
project.project_group_links.create(
|
||||
group: group,
|
||||
group_access: Gitlab::Access::DEVELOPER
|
||||
)
|
||||
|
||||
group.add_user(master, Gitlab::Access::MASTER)
|
||||
group.add_user(reporter, Gitlab::Access::REPORTER)
|
||||
end
|
||||
|
||||
it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
|
||||
it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
|
||||
it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
|
||||
end
|
||||
|
||||
describe :max_member_access do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:empty_project) }
|
||||
|
||||
before do
|
||||
project.project_group_links.create(
|
||||
group: group,
|
||||
group_access: Gitlab::Access::DEVELOPER
|
||||
)
|
||||
|
||||
group.add_user(master, Gitlab::Access::MASTER)
|
||||
group.add_user(reporter, Gitlab::Access::REPORTER)
|
||||
end
|
||||
|
||||
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
|
||||
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
|
||||
it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
|
||||
|
||||
it "does not have an access" do
|
||||
project.namespace.update(share_with_group_lock: true)
|
||||
expect(project.team.max_member_access(master.id)).to be_nil
|
||||
expect(project.team.max_member_access(reporter.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#human_max_access" do
|
||||
it 'returns Master role' do
|
||||
user = create(:user)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
describe 'GET /projects/:project_id/snippets/:id' do
|
||||
# TODO (rspeicher): Deprecated; remove in 9.0
|
||||
it 'always exposes expires_at as nil' do
|
||||
admin = create(:admin)
|
||||
snippet = create(:project_snippet, author: admin)
|
||||
|
||||
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
|
||||
|
||||
expect(json_response).to have_key('expires_at')
|
||||
expect(json_response['expires_at']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -747,6 +747,42 @@ describe API::API, api: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /projects/:id/share" do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
it "should share project with group" do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
|
||||
end.to change { ProjectGroupLink.count }.by(1)
|
||||
|
||||
expect(response.status).to eq 201
|
||||
expect(json_response['group_id']).to eq group.id
|
||||
expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
|
||||
end
|
||||
|
||||
it "should return a 400 error when group id is not given" do
|
||||
post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
|
||||
expect(response.status).to eq 400
|
||||
end
|
||||
|
||||
it "should return a 400 error when access level is not given" do
|
||||
post api("/projects/#{project.id}/share", user), group_id: group.id
|
||||
expect(response.status).to eq 400
|
||||
end
|
||||
|
||||
it "should return a 400 error when sharing is disabled" do
|
||||
project.namespace.update(share_with_group_lock: true)
|
||||
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
|
||||
expect(response.status).to eq 400
|
||||
end
|
||||
|
||||
it "should return a 409 error when wrong params passed" do
|
||||
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
|
||||
expect(response.status).to eq 409
|
||||
expect(json_response['message']).to eq 'Group access is not included in the list'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/search/:query' do
|
||||
let!(:query) { 'query'}
|
||||
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
|
||||
|
|
Loading…
Reference in New Issue