Commit 88f8d3a4 authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Merge branch 'master' into 4009-external-users

parents 76eeb316 b782e7c9
...@@ -31,12 +31,14 @@ v 8.6.0 (unreleased) ...@@ -31,12 +31,14 @@ v 8.6.0 (unreleased)
- Add ability to show archived projects on dashboard, explore and group pages - Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page - Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted - 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 v 8.5.5
- Ensure removing a project removes associated Todo entries - Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed - Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages - Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior - Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
......
...@@ -104,6 +104,8 @@ class Dispatcher ...@@ -104,6 +104,8 @@ class Dispatcher
new ProjectFork() new ProjectFork()
when 'projects:artifacts:browse' when 'projects:artifacts:browse'
new BuildArtifacts() new BuildArtifacts()
when 'projects:group_links:index'
new GroupsSelect()
switch path.first() switch path.first()
when 'admin' when 'admin'
......
...@@ -238,13 +238,15 @@ class GitLabDropdown ...@@ -238,13 +238,15 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex] selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if !value?
field.remove()
if @options.multiSelect if @options.multiSelect
oldValue = field.val() oldValue = field.val()
if oldValue if oldValue
value = "#{oldValue},#{value}" value = "#{oldValue},#{value}"
else else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
field.remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.toggleClass "is-active"
......
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 ...@@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@shared_projects = @group.shared_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -133,7 +135,7 @@ class GroupsController < Groups::ApplicationController ...@@ -133,7 +135,7 @@ class GroupsController < Groups::ApplicationController
end end
def group_params 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 end
def load_events def load_events
......
class Projects::ForksController < Projects::ApplicationController class Projects::ForksController < Projects::ApplicationController
include ContinueParams
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
render :error render :error
end end
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 end
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
class Projects::ImportsController < Projects::ApplicationController class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create] before_action :require_no_repo, only: [:new, :create]
...@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
private 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 def finished_notice
if @project.forked? if @project.forked?
'The project was successfully forked.' 'The project was successfully forked.'
......
...@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
@project_member = @project.project_members.new @project_member = @project.project_members.new
@project_group_links = @project.project_group_links
end end
def create def create
......
...@@ -244,10 +244,17 @@ class IssuableFinder ...@@ -244,10 +244,17 @@ class IssuableFinder
items items
end end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil]) 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 else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
...@@ -42,16 +42,14 @@ class ProjectsFinder ...@@ -42,16 +42,14 @@ class ProjectsFinder
def group_projects(current_user, group) def group_projects(current_user, group)
return [group.projects.public_only] unless current_user 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? if current_user.external?
[ user_group_projects.push(group.projects.public_only)
group_projects_for_user(current_user, group),
group.projects.public_only
]
else else
[ user_group_projects.push(group.projects.public_and_internal_only)
group_projects_for_user(current_user, group),
group.projects.public_and_internal_only
]
end end
end end
......
...@@ -59,6 +59,7 @@ module MilestonesHelper ...@@ -59,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
......
...@@ -23,6 +23,8 @@ class Group < Namespace ...@@ -23,6 +23,8 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members alias_method :members, :group_members
has_many :users, through: :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? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
......
...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id) MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId include InternalId
include Sortable include Sortable
...@@ -81,6 +82,10 @@ class Milestone < ActiveRecord::Base ...@@ -81,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/) super("milestones", /(?<milestone>\d+)/)
end end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]") escaped_title = self.title.gsub("]", "\\]")
......
...@@ -173,26 +173,29 @@ class Note < ActiveRecord::Base ...@@ -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) Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end end
# Check if such line of code exists in merge request diff # Check if this note is part of an "active" discussion
# If exists - its active discussion #
# If not - its outdated diff # 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? def active?
return true unless self.diff return true unless self.diff
return false unless noteable return false unless noteable
return @active if defined?(@active) return @active if defined?(@active)
diffs = noteable.diffs(Commit.max_diff_options) noteable_diff = find_noteable_diff
notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
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) @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
# We cannot use ||= because @active may be false else
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } @active = false
end end
def outdated? @active
!active?
end end
def diff_file_index def diff_file_index
...@@ -380,6 +383,12 @@ class Note < ActiveRecord::Base ...@@ -380,6 +383,12 @@ class Note < ActiveRecord::Base
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end 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? def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line? (for_issue? || for_merge_request?) && !for_diff_line?
end end
......
...@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base ...@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects 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_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -893,6 +895,10 @@ class Project < ActiveRecord::Base ...@@ -893,6 +895,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active jira_tracker? && jira_service.active
end end
def allowed_to_share_with_group?
!namespace.share_with_group_lock
end
def ci_commit(sha) def ci_commit(sha)
ci_commits.find_by(sha: sha) ci_commits.find_by(sha: sha)
end end
......
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 ...@@ -160,7 +160,27 @@ class ProjectTeam
end end
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 end
private private
...@@ -168,6 +188,35 @@ class ProjectTeam ...@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil) def fetch_members(level = nil)
project_members = project.project_members project_members = project.project_members
group_members = group ? group.group_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 if level
project_members = project_members.send(level) project_members = project_members.send(level)
...@@ -175,6 +224,7 @@ class ProjectTeam ...@@ -175,6 +224,7 @@ class ProjectTeam
end end
user_ids = project_members.pluck(:user_id) 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_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids) User.where(id: user_ids)
......
...@@ -838,7 +838,8 @@ class User < ActiveRecord::Base ...@@ -838,7 +838,8 @@ class User < ActiveRecord::Base
def projects_union def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id), Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id), groups_projects.select(:id),
projects.select(:id)]) projects.select(:id),
groups.joins(:shared_projects).select(:project_id)])
end end
def ci_projects_union def ci_projects_union
......
...@@ -50,6 +50,22 @@ ...@@ -50,6 +50,22 @@
.panel-footer .panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab' = 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 .col-md-6
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.panel.panel-default .panel.panel-default
......
- 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 @@ ...@@ -23,6 +23,15 @@
%hr %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" = 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 .form-actions
= f.submit 'Save group', class: "btn btn-save" = f.submit 'Save group', class: "btn btn-save"
......
...@@ -32,6 +32,10 @@ ...@@ -32,6 +32,10 @@
%li.active %li.active
= link_to "#projects", 'data-toggle' => 'tab' do = link_to "#projects", 'data-toggle' => 'tab' do
Projects Projects
- if @shared_projects.present?
%li
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
- if can?(current_user, :read_group, @group) - if can?(current_user, :read_group, @group)
%div{ class: container_class } %div{ class: container_class }
...@@ -39,6 +43,9 @@ ...@@ -39,6 +43,9 @@
.tab-pane.active#projects .tab-pane.active#projects
= render "projects", projects: @projects = render "projects", projects: @projects
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
- else - else
%p.nav-links.no-top %p.nav-links.no-top
No projects to show No projects to show
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('home fw') = icon('bookmark fw')
%span %span
Project Project
= nav_link(path: 'projects#activity') do = nav_link(path: 'projects#activity') do
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
= icon('pencil-square-o fw') = icon('pencil-square-o fw')
%span %span
Project Settings 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 = nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw') = icon('key fw')
......
- 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"
- @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 @@ ...@@ -18,3 +18,6 @@
- if @group - if @group
= render "group_members", members: @group_members = 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 @@ ...@@ -15,7 +15,7 @@
.filter-item.inline .filter-item.inline
- if params[:assignee_id] - if params[:assignee_id]
= hidden_field_tag(:assignee_id, 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" } }) 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 .filter-item.inline.milestone-filter
......
...@@ -701,6 +701,8 @@ Rails.application.routes.draw do ...@@ -701,6 +701,8 @@ Rails.application.routes.draw do
end end
end end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do member do
delete :delete_attachment delete :delete_attachment
......
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
class AddAccessToProjectGroupLink < ActiveRecord::Migration
def change
add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
end
end
class AddGroupShareLock < ActiveRecord::Migration
def change
add_column :namespaces, :share_with_group_lock, :boolean, default: false
end
end
...@@ -578,14 +578,15 @@ ActiveRecord::Schema.define(version: 20160310185910) do ...@@ -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"} add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t| create_table "namespaces", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "path", null: false t.string "path", null: false
t.integer "owner_id" t.integer "owner_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "type" t.string "type"
t.string "description", default: "", null: false t.string "description", default: "", null: false
t.string "avatar" t.string "avatar"
t.boolean "share_with_group_lock", default: false
end end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree 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 ...@@ -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", ["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 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| create_table "project_import_data", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
t.text "data" t.text "data"
...@@ -765,9 +774,9 @@ ActiveRecord::Schema.define(version: 20160310185910) do ...@@ -765,9 +774,9 @@ ActiveRecord::Schema.define(version: 20160310185910) do
t.string "type" t.string "type"
t.string "title" t.string "title"
t.integer "project_id" t.integer "project_id"
t.datetime "created_at", null: false t.datetime "created_at"
t.datetime "updated_at", null: false t.datetime "updated_at"
t.boolean "active", null: false t.boolean "active", default: false, null: false
t.text "properties" t.text "properties"
t.boolean "template", default: false t.boolean "template", default: false
t.boolean "push_events", default: true t.boolean "push_events", default: true
......
...@@ -145,6 +145,7 @@ Parameters: ...@@ -145,6 +145,7 @@ Parameters:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z" "created_at": "2013-09-30T13:46:01Z"
}, },
"expires_at": null,
"updated_at": "2013-10-02T07:34:20Z", "updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z" "created_at": "2013-10-02T07:34:20Z"
} }
......
...@@ -51,6 +51,7 @@ Parameters: ...@@ -51,6 +51,7 @@ Parameters:
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z" "created_at": "2012-05-23T08:00:58Z"
}, },
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z", "updated_at": "2012-06-28T10:52:04Z",
"created_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 ...@@ -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 Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure. 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 ## Hooks
Also called Project Hooks and Webhooks. Also called Project Hooks and Webhooks.
......
...@@ -233,9 +233,9 @@ sudo usermod -aG redis git ...@@ -233,9 +233,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
......
# 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 ...@@ -582,6 +582,7 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC", "created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC", "updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb", "file_name": "test.rb",
"expires_at": null,
"type": "ProjectSnippet", "type": "ProjectSnippet",
"visibility_level": 0 "visibility_level": 0
} }
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
- [Project forking workflow](forking_workflow.md) - [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md) - [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.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) - [Web Editor](web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
......
# 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'.
# 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)
...@@ -21,6 +21,11 @@ Feature: Admin Groups ...@@ -21,6 +21,11 @@ Feature: Admin Groups
When I select user "John Doe" from user list as "Reporter" 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" 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 @javascript
Scenario: Remove user from group Scenario: Remove user from group
Given we have user "John Doe" in group Given we have user "John Doe" in group
......
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 ...@@ -34,9 +34,10 @@ Feature: Project Network Graph
@javascript @javascript
Scenario: I should filter selected tag Scenario: I should filter selected tag
When I switch ref to "v1.0.0" 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" Then page should have content not containing "v1.0.0"
When click "Show only selected branch" checkbox 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 When click "Show only selected branch" checkbox
Then page should have content not containing "v1.0.0" Then page should have content not containing "v1.0.0"
......
...@@ -39,3 +39,8 @@ Feature: Project Team Management ...@@ -39,3 +39,8 @@ Feature: Project Team Management
And I click link "Import team from another project" And I click link "Import team from another project"
And I submit "Website" project for import team And I submit "Website" project for import team
Then I should see "Mike" in team list as "Reporter" 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 ...@@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end end
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 step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john) current_group.add_reporter(user_john)
end end
...@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps ...@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
@group ||= Group.first @group ||= Group.first
end end
def shared_project
@shared_project ||= create(:empty_project)
end
def user_john def user_john
@user_john ||= User.find_by(name: "John Doe") @user_john ||= User.find_by(name: "John Doe")
end end
......
...@@ -36,22 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps ...@@ -36,22 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end end
step 'I click "Authored by me" link' do step 'I click "Authored by me" link' do
execute_script('$("#assignee_id").val("")') find("#assignee_id").set("")
execute_script('$(".js-user-search").first().click()') find(".js-author-search", match: :first).click
sleep 1 find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 1
end end
step 'I click "All" link' do step 'I click "All" link' do
execute_script('$(".js-user-search").first().click()') find('.js-author-search').click
sleep 1 find('.dropdown-menu-user-full-name', match: :first).click
execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
sleep 1 find('.js-assignee-search').click
execute_script('$(".js-user-search").eq(1).click()') find('.dropdown-menu-user-full-name', match: :first).click
sleep 1
execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
sleep 1
end end
def should_see(issue) def should_see(issue)
......
...@@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps ...@@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end end
step 'I click "Authored by me" link' do step 'I click "Authored by me" link' do
execute_script('$("#assignee_id").val("")') find("#assignee_id").set("")
execute_script('$(".js-user-search").first().click()') find(".js-author-search", match: :first).click
sleep 0.5 find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
sleep 2
end end
step 'I click "All" link' do step 'I click "All" link' do
execute_script('$(".js-user-search").first().click()') find(".js-author-search").click
sleep 0.5 find(".dropdown-menu-author li a", match: :first).click
execute_script('$(".js-user-search").first().parent().find("li a").first().click()') find(".js-assignee-search").click
sleep 2 find(".dropdown-menu-assignee li a", match: :first).click
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
end end
def should_see(merge_request) def should_see(merge_request)
......
...@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps ...@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
When 'I switch ref to "feature"' do When 'I switch ref to "feature"' do
select 'feature', from: 'ref' select 'feature', from: 'ref'
sleep 2
end end
When 'I switch ref to "v1.0.0"' do When 'I switch ref to "v1.0.0"' do
select 'v1.0.0', from: 'ref' select 'v1.0.0', from: 'ref'
sleep 2
end end
When 'click "Show only selected branch" checkbox' do When 'click "Show only selected branch" checkbox' do
find('#filter_ref').click find('#filter_ref').click
sleep 2
end end
step 'page should have content not containing "v1.0.0"' do step 'page should have content not containing "v1.0.0"' do
...@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps ...@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end end
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 page.within '.network-graph' do
expect(page).not_to have_content 'Change some files' expect(page).not_to have_content 'Change some files'
end end
......
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 ...@@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link('Remove user from team') click_link('Remove user from team')
end end
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 end
...@@ -144,6 +144,9 @@ module API ...@@ -144,6 +144,9 @@ module API
expose :id, :title, :file_name expose :id, :title, :file_name
expose :author, using: Entities::UserBasic expose :author, using: Entities::UserBasic
expose :updated_at, :created_at expose :updated_at, :created_at
# TODO (rspeicher): Deprecated; remove in 9.0
expose(:expires_at) { |snippet| nil }
end end
class ProjectEntity < Grape::Entity class ProjectEntity < Grape::Entity
...@@ -243,6 +246,10 @@ module API ...@@ -243,6 +246,10 @@ module API
end end
end end
class ProjectGroupLink < Grape::Entity
expose :id, :project_id, :group_id, :group_access
end
class Namespace < Grape::Entity class Namespace < Grape::Entity
expose :id, :path, :kind expose :id, :path, :kind
end end
......
...@@ -290,6 +290,33 @@ module API ...@@ -290,6 +290,33 @@ module API
end end
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 # Upload a file
# #
# Parameters: # Parameters:
......
...@@ -19,7 +19,7 @@ describe Projects::ImportsController do ...@@ -19,7 +19,7 @@ describe Projects::ImportsController do
end end
it 'sets flash.now if params is present' do 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' expect(flash.now[:notice]).to eq 'Started'
end end
...@@ -45,7 +45,7 @@ describe Projects::ImportsController do ...@@ -45,7 +45,7 @@ describe Projects::ImportsController do
end end
it 'sets flash.now if params is present' do 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' expect(flash.now[:notice]).to eq 'In progress'
end end
......
FactoryGirl.define do
factory :project_group_link do
project
group
end
end
...@@ -17,6 +17,10 @@ describe ProjectsFinder do ...@@ -17,6 +17,10 @@ describe ProjectsFinder do
create(:project, :public, group: group, name: 'C', path: 'C') create(:project, :public, group: group, name: 'C', path: 'C')
end end
let!(:shared_project) do
create(:project, :private, name: 'D', path: 'D')
end
let(:finder) { described_class.new } let(:finder) { described_class.new }
describe 'without a group' do describe 'without a group' do
...@@ -56,7 +60,35 @@ describe ProjectsFinder do ...@@ -56,7 +60,35 @@ describe ProjectsFinder do
describe 'with a user' do describe 'with a user' do
subject { finder.execute(user, group: group) } 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 end
end end
......
...@@ -152,7 +152,7 @@ describe Note, models: true do ...@@ -152,7 +152,7 @@ describe Note, models: true do
end end
end end
describe :grouped_awards do describe '.grouped_awards' do
before do before do
create :note, note: "smile", is_award: true create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true create :note, note: "smile", is_award: true
...@@ -169,6 +169,66 @@ describe Note, models: true do ...@@ -169,6 +169,66 @@ describe Note, models: true do
end end
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 describe "editable?" do
it "returns true" do it "returns true" do
note = build(:note) note = build(:note)
......
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 ...@@ -67,6 +67,50 @@ describe ProjectTeam, models: true do
end end
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 describe "#human_max_access" do
it 'returns Master role' do it 'returns Master role' do
user = create(:user) user = create(:user)
......
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 ...@@ -747,6 +747,42 @@ describe API::API, api: true do
end end
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 describe 'GET /projects/search/:query' do
let!(:query) { 'query'} let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment