Commit a42d469a authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'atom-feed-latest-update' into 'master'

Improve performance of user profiles, finding groups, and finding projects

This MR improves the following:

* Rendering of profile pages and Atom feeds
* Finding groups (using GroupsFinder & friends)
* Finding projects (using ProjectsFinder & friends)

Initially this MR was intended to only improve rendering of Atom feeds, but over time other fixes were introduced as well as the same code was the cause of all these problems.

See merge request !1790
parents 5b302854 96cdacd4
...@@ -3,6 +3,8 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -3,6 +3,8 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
v 8.2.0 v 8.2.0
- Improved performance of finding projects and groups in various places
- Improved performance of rendering user profile pages and Atom feeds
- Fix grouping of contributors by email in graph. - Fix grouping of contributors by email in graph.
- Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
- Fix Drone CI service template not saving properly (Stan Hu) - Fix Drone CI service template not saving properly (Stan Hu)
......
...@@ -3,14 +3,11 @@ class UsersController < ApplicationController ...@@ -3,14 +3,11 @@ class UsersController < ApplicationController
before_action :set_user before_action :set_user
def show def show
@contributed_projects = contributed_projects.joined(@user). @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
reject(&:forked?)
@projects = @user.personal_projects. @projects = PersonalProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids).includes(:namespace)
# Collect only groups common for both users @groups = JoinedGroupsFinder.new(@user).execute(current_user)
@groups = @user.groups & GroupsFinder.new.execute(current_user)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -53,16 +50,8 @@ class UsersController < ApplicationController ...@@ -53,16 +50,8 @@ class UsersController < ApplicationController
@user = User.find_by_username!(params[:username]) @user = User.find_by_username!(params[:username])
end end
def authorized_projects_ids
# Projects user can view
@authorized_projects_ids ||=
ProjectsFinder.new.execute(current_user).pluck(:id)
end
def contributed_projects def contributed_projects
@contributed_projects = Project. ContributedProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids & @user.contributed_projects_ids).
includes(:namespace)
end end
def contributions_calendar def contributions_calendar
...@@ -73,9 +62,13 @@ class UsersController < ApplicationController ...@@ -73,9 +62,13 @@ class UsersController < ApplicationController
def load_events def load_events
# Get user activity feed for projects common for both users # Get user activity feed for projects common for both users
@events = @user.recent_events. @events = @user.recent_events.
where(project_id: authorized_projects_ids). merge(projects_for_current_user).
with_associations references(:project).
with_associations.
limit_recent(20, params[:offset])
end
@events = @events.limit(20).offset(params[:offset] || 0) def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end end
end end
class ContributedProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects "@user" contributed to, limited to either public projects
# or projects visible to the given user.
#
# current_user - When given the list of the projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.contributed_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.contributed_projects.public_only
end
end
class GroupsFinder class GroupsFinder
def execute(current_user, options = {}) # Finds the groups available to the given user.
all_groups(current_user) #
# current_user - The user to find the groups for.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end end
private private
def all_groups(current_user) # This method returns the groups "current_user" can see.
group_ids = if current_user def groups_visible_to_user(current_user)
if current_user.authorized_groups.any? base = groups_for_projects(public_and_internal_projects)
# User has access to groups
# union = Gitlab::SQL::Union.
# Return only: new([base.select(:id), current_user.authorized_groups.select(:id)])
# groups with public projects
# groups with internal projects Group.where("namespaces.id IN (#{union.to_sql})")
# groups with joined projects
#
Project.public_and_internal_only.pluck(:namespace_id) +
current_user.authorized_groups.pluck(:id)
else
# User has no group membership
#
# Return only:
# groups with public projects
# groups with internal projects
#
Project.public_and_internal_only.pluck(:namespace_id)
end end
else
# Not authenticated def public_groups
# groups_for_projects(public_projects)
# Return only: end
# groups with public projects
Project.public_only.pluck(:namespace_id) def groups_for_projects(projects)
Group.public_and_given_groups(projects.select(:namespace_id))
end
def public_projects
Project.unscoped.public_only
end end
Group.where("public IS TRUE OR id IN(?)", group_ids) def public_and_internal_projects
Project.unscoped.public_and_internal_only
end end
end end
# Class for finding the groups a user is a member of.
class JoinedGroupsFinder
def initialize(user = nil)
@user = user
end
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
#
# current_user - If given the groups of "@user" will only include the groups
# "current_user" can also see.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end
private
# Returns the groups the user in "current_user" can see.
#
# This list includes all public/internal projects as well as the projects of
# "@user" that "current_user" also has access to.
def groups_visible_to_user(current_user)
base = @user.authorized_groups.visible_to_user(current_user)
extra = public_and_internal_groups
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Group.where("namespaces.id IN (#{union.to_sql})")
end
def public_groups
groups_for_projects(@user.authorized_projects.public_only)
end
def public_and_internal_groups
groups_for_projects(@user.authorized_projects.public_and_internal_only)
end
def groups_for_projects(projects)
@user.groups.public_and_given_groups(projects.select(:namespace_id))
end
end
class PersonalProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects belonging to the user in "@user", limited to either
# public projects or projects visible to the given user.
#
# current_user - When given the list of projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.personal_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_and_internal_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.personal_projects.public_only
end
def public_and_internal_projects
@user.personal_projects.public_and_internal_only
end
end
class ProjectsFinder class ProjectsFinder
def execute(current_user, options = {}) # Returns all projects, optionally including group projects a user has access
# to.
#
# ## Examples
#
# Retrieving all public projects:
#
# ProjectsFinder.new.execute
#
# Retrieving all public/internal projects and those the given user has access
# to:
#
# ProjectsFinder.new.execute(some_user)
#
# Retrieving all public/internal projects as well as the group's projects the
# user has access to:
#
# ProjectsFinder.new.execute(some_user, group: some_group)
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil, options = {})
group = options[:group] group = options[:group]
if group if group
group_projects(current_user, group) base, extra = group_projects(current_user, group)
else else
all_projects(current_user) base, extra = all_projects(current_user)
end
if base and extra
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
else
base
end end
end end
...@@ -13,77 +41,36 @@ class ProjectsFinder ...@@ -13,77 +41,36 @@ class ProjectsFinder
def group_projects(current_user, group) def group_projects(current_user, group)
if current_user if current_user
if group.users.include?(current_user) [
# User is group member group_projects_for_user(current_user, group),
#
# Return ALL group projects
group.projects
else
projects_members = ProjectMember.in_projects(group.projects).
with_user(current_user)
if projects_members.any?
# User is a project member
#
# Return only:
# public projects
# internal projects
# joined projects
#
group.projects.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
projects_members.pluck(:source_id),
Project.public_and_internal_levels
)
else
# User has no access to group or group projects
#
# Return only:
# public projects
# internal projects
#
group.projects.public_and_internal_only group.projects.public_and_internal_only
end ]
end
else else
# Not authenticated [group.projects.public_only]
#
# Return only:
# public projects
group.projects.public_only
end end
end end
def all_projects(current_user) def all_projects(current_user)
if current_user if current_user
if current_user.authorized_projects.any? [current_user.authorized_projects, public_and_internal_projects]
# User has access to private projects
#
# Return only:
# public projects
# internal projects
# joined projects
#
Project.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
current_user.authorized_projects.pluck(:id),
Project.public_and_internal_levels
)
else else
# User has no access to private projects [Project.public_only]
#
# Return only:
# public projects
# internal projects
#
Project.public_and_internal_only
end end
end
def group_projects_for_user(current_user, group)
if group.users.include?(current_user)
group.projects
else else
# Not authenticated group.projects.visible_to_user(current_user)
#
# Return only:
# public projects
Project.public_only
end end
end end
def public_projects
Project.unscoped.public_only
end
def public_and_internal_projects
Project.unscoped.public_and_internal_only
end
end end
...@@ -8,8 +8,9 @@ module Sortable ...@@ -8,8 +8,9 @@ module Sortable
included do included do
# By default all models should be ordered # By default all models should be ordered
# by created_at field starting from newest # by created_at field starting from newest
default_scope { order(id: :desc) } default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) }
......
...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base ...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
Event::PUSHED, ["MergeRequest", "Issue"], Event::PUSHED, ["MergeRequest", "Issue"],
[Event::CREATED, Event::CLOSED, Event::MERGED]) [Event::CREATED, Event::CLOSED, Event::MERGED])
end end
def latest_update_time
row = select(:updated_at, :project_id).reorder(id: :desc).take
row ? row.updated_at : nil
end
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
end end
def proper? def proper?
......
...@@ -49,6 +49,14 @@ class Group < Namespace ...@@ -49,6 +49,14 @@ class Group < Namespace
def reference_pattern def reference_pattern
User.reference_pattern User.reference_pattern
end end
def public_and_given_groups(ids)
where('public IS TRUE OR namespaces.id IN (?)', ids)
end
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
end end
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
......
...@@ -286,6 +286,10 @@ class Project < ActiveRecord::Base ...@@ -286,6 +286,10 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
def visible_to_user(user)
where(id: user.authorized_projects.select(:id).reorder(nil))
end
end end
def team def team
......
...@@ -389,42 +389,23 @@ class User < ActiveRecord::Base ...@@ -389,42 +389,23 @@ class User < ActiveRecord::Base
end end
end end
# Groups user has access to # Returns the groups a user has access to
def authorized_groups def authorized_groups
@authorized_groups ||= begin union = Gitlab::SQL::Union.
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) new([groups.select(:id), authorized_projects.select(:namespace_id)])
Group.where(id: group_ids)
end
end
def authorized_projects_id Group.where("namespaces.id IN (#{union.to_sql})")
@authorized_projects_id ||= begin
project_ids = personal_projects.pluck(:id)
project_ids.push(*groups_projects.pluck(:id))
project_ids.push(*projects.pluck(:id).uniq)
end
end end
def master_or_owner_projects_id # Returns the groups a user is authorized to access.
@master_or_owner_projects_id ||= begin
scope = { access_level: [ Gitlab::Access::MASTER, Gitlab::Access::OWNER ] }
project_ids = personal_projects.pluck(:id)
project_ids.push(*groups_projects.where(members: scope).pluck(:id))
project_ids.push(*projects.where(members: scope).pluck(:id).uniq)
end
end
# Projects user has access to
def authorized_projects def authorized_projects
@authorized_projects ||= Project.where(id: authorized_projects_id) Project.where("projects.id IN (#{projects_union.to_sql})")
end end
def owned_projects def owned_projects
@owned_projects ||= @owned_projects ||=
begin Project.where('namespace_id IN (?) OR namespace_id = ?',
namespace_ids = owned_groups.pluck(:id).push(namespace.id) owned_groups.select(:id), namespace.id).joins(:namespace)
Project.in_namespace(namespace_ids).joins(:namespace)
end
end end
# Team membership in authorized projects # Team membership in authorized projects
...@@ -739,12 +720,25 @@ class User < ActiveRecord::Base ...@@ -739,12 +720,25 @@ class User < ActiveRecord::Base
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
end end
def contributed_projects_ids # Returns the projects a user contributed to in the last year.
Event.contributions.where(author_id: self). #
# This method relies on a subquery as this performs significantly better
# compared to a JOIN when coupled with, for example,
# `Project.visible_to_user`. That is, consider the following code:
#
# some_user.contributed_projects.visible_to_user(other_user)
#
# If this method were to use a JOIN the resulting query would take roughly 200
# ms on a database with a similar size to GitLab.com's database. On the other
# hand, using a subquery means we can get the exact same data in about 40 ms.
def contributed_projects
events = Event.select(:project_id).
contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year). where("created_at > ?", Time.now - 1.year).
reorder(project_id: :desc). uniq.
select(:project_id). reorder(nil)
uniq.map(&:project_id)
Project.where(id: events)
end end
def restricted_signup_domains def restricted_signup_domains
...@@ -777,8 +771,27 @@ class User < ActiveRecord::Base ...@@ -777,8 +771,27 @@ class User < ActiveRecord::Base
def ci_authorized_runners def ci_authorized_runners
@ci_authorized_runners ||= begin @ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.joins(:project). runner_ids = Ci::RunnerProject.joins(:project).
where(ci_projects: { gitlab_id: master_or_owner_projects_id }).select(:runner_id) where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids) Ci::Runner.specific.where(id: runner_ids)
end end
end end
private
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
projects.select(:id)])
end
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
other = projects.where(members: scope)
Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
other.select(:id)])
end
end end
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url xml.id dashboard_projects_url
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group) xml.id group_url(@group)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project) xml.id namespace_project_url(@project.namespace, @project)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user) xml.id user_url(@user)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
class AddProjectsPublicIndex < ActiveRecord::Migration
def change
add_index :namespaces, :public
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151116144118) do ActiveRecord::Schema.define(version: 20151118162244) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -538,6 +538,7 @@ ActiveRecord::Schema.define(version: 20151116144118) do ...@@ -538,6 +538,7 @@ ActiveRecord::Schema.define(version: 20151116144118) do
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: true do |t| create_table "notes", force: true do |t|
......
module Gitlab
module SQL
# Class for building SQL UNION statements.
#
# ORDER BYs are dropped from the relations as the final sort order is not
# guaranteed any way.
#
# Example usage:
#
# union = Gitlab::SQL::Union.new(user.personal_projects, user.projects)
# sql = union.to_sql
#
# Project.where("id IN (#{sql})")
class Union
def initialize(relations)
@relations = relations
end
def to_sql
# Some relations may include placeholders for prepared statements, these
# aren't incremented properly when joining relations together this way.
# By using "unprepared_statements" we remove the usage of placeholders
# (thus fixing this problem), at a slight performance cost.
fragments = ActiveRecord::Base.connection.unprepared_statement do
@relations.map do |rel|
rel.reorder(nil).to_sql
end
end
fragments.join("\nUNION\n")
end
end
end
end
...@@ -16,15 +16,28 @@ describe UsersController do ...@@ -16,15 +16,28 @@ describe UsersController do
context 'with rendered views' do context 'with rendered views' do
render_views render_views
it 'renders the show template' do describe 'when logged in' do
before do
sign_in(user) sign_in(user)
end
it 'renders the show template' do
get :show, username: user.username get :show, username: user.username
expect(response).to be_success expect(response).to be_success
expect(response).to render_template('show') expect(response).to render_template('show')
end end
end end
describe 'when logged out' do
it 'renders the show template' do
get :show, username: user.username
expect(response).to be_success
expect(response).to render_template('show')
end
end
end
end end
describe 'GET #calendar' do describe 'GET #calendar' do
......
require 'spec_helper'
describe ContributedProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
let!(:public_project) { create(:project, :public) }
let!(:private_project) { create(:project, :private) }
before do
private_project.team << [source_user, Gitlab::Access::MASTER]
private_project.team << [current_user, Gitlab::Access::DEVELOPER]
public_project.team << [source_user, Gitlab::Access::MASTER]
create(:event, action: Event::PUSHED, project: public_project,
target: public_project, author: source_user)
create(:event, action: Event::PUSHED, project: private_project,
target: private_project, author: source_user)
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([public_project]) }
end
describe 'with a current user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
end
end
require 'spec_helper'
describe GroupsFinder do
let(:user) { create :user }
let!(:group) { create :group }
let!(:public_group) { create :group, public: true }
describe :execute do
it 'finds public group' do
groups = GroupsFinder.new.execute(user)
expect(groups.size).to eq(1)
expect(groups.first).to eq(public_group)
end
end
end
require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
let(:group1) { create(:group) }
let(:group2) { create(:group) }
let(:group3) { create(:group) }
let(:group4) { create(:group, public: true) }
let!(:public_project) { create(:project, :public, group: group1) }
let!(:internal_project) { create(:project, :internal, group: group2) }
let!(:private_project) { create(:project, :private, group: group3) }
let(:finder) { described_class.new }
describe 'with a user' do
subject { finder.execute(user) }
describe 'when the user is not a member of any groups' do
it { is_expected.to eq([group4, group2, group1]) }
end
describe 'when the user is a member of a group' do
before do
group3.add_user(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to eq([group4, group3, group2, group1]) }
end
describe 'when the user is a member of a private project' do
before do
private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to eq([group4, group3, group2, group1]) }
end
end
describe 'without a user' do
subject { finder.execute }
it { is_expected.to eq([group4, group1]) }
end
end
end
require 'spec_helper'
describe JoinedGroupsFinder do
describe '#execute' do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:group1) { create(:group) }
let(:group2) { create(:group) }
let(:group3) { create(:group) }
let(:group4) { create(:group, public: true) }
let!(:public_project) { create(:project, :public, group: group1) }
let!(:internal_project) { create(:project, :internal, group: group2) }
let!(:private_project) { create(:project, :private, group: group3) }
let(:finder) { described_class.new(source_user) }
before do
[group1, group2, group3, group4].each do |group|
group.add_user(source_user, Gitlab::Access::MASTER)
end
end
describe 'with a current user' do
describe 'when the current user has access to the projects of the source user' do
before do
private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
end
subject { finder.execute(current_user) }
it { is_expected.to eq([group4, group3, group2, group1]) }
end
describe 'when the current user does not have access to the projects of the source user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([group4, group2, group1]) }
end
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([group4, group1]) }
end
end
end
require 'spec_helper'
describe PersonalProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
let!(:public_project) do
create(:project, :public, namespace: source_user.namespace, name: 'A',
path: 'A')
end
let!(:private_project) do
create(:project, :private, namespace: source_user.namespace, name: 'B',
path: 'B')
end
before do
private_project.team << [current_user, Gitlab::Access::DEVELOPER]
end
describe 'without a current user' do
subject { finder.execute }
it { is_expected.to eq([public_project]) }
end
describe 'with a current user' do
subject { finder.execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
end
end
require 'spec_helper' require 'spec_helper'
describe ProjectsFinder do describe ProjectsFinder do
let(:user) { create :user } describe '#execute' do
let(:group) { create :group } let(:user) { create(:user) }
let(:project1) { create(:empty_project, :public, group: group) } let!(:private_project) { create(:project, :private) }
let(:project2) { create(:empty_project, :internal, group: group) } let!(:internal_project) { create(:project, :internal) }
let(:project3) { create(:empty_project, :private, group: group) } let!(:public_project) { create(:project, :public) }
let(:project4) { create(:empty_project, :private, group: group) }
context 'non authenticated' do let(:finder) { described_class.new }
subject { ProjectsFinder.new.execute(nil, group: group) }
it { is_expected.to include(project1) } describe 'without a group' do
it { is_expected.not_to include(project2) } describe 'without a user' do
it { is_expected.not_to include(project3) } subject { finder.execute }
it { is_expected.not_to include(project4) }
it { is_expected.to eq([public_project]) }
end end
context 'authenticated' do describe 'with a user' do
subject { ProjectsFinder.new.execute(user, group: group) } subject { finder.execute(user) }
it { is_expected.to include(project1) } describe 'without private projects' do
it { is_expected.to include(project2) } it { is_expected.to eq([public_project, internal_project]) }
it { is_expected.not_to include(project3) }
it { is_expected.not_to include(project4) }
end end
context 'authenticated, project member' do describe 'with private projects' do
before { project3.team << [user, :developer] } before do
private_project.team.add_user(user, Gitlab::Access::MASTER)
subject { ProjectsFinder.new.execute(user, group: group) } end
it { is_expected.to include(project1) } it do
it { is_expected.to include(project2) } is_expected.to eq([public_project, internal_project,
it { is_expected.to include(project3) } private_project])
it { is_expected.not_to include(project4) } end
end
end
end end
context 'authenticated, group member' do describe 'with a group' do
before { group.add_developer(user) } let(:group) { public_project.group }
subject { ProjectsFinder.new.execute(user, group: group) } describe 'without a user' do
subject { finder.execute(nil, group: group) }
it { is_expected.to include(project1) } it { is_expected.to eq([public_project]) }
it { is_expected.to include(project2) } end
it { is_expected.to include(project3) }
it { is_expected.to include(project4) } describe 'with a user' do
subject { finder.execute(user, group: group) }
it { is_expected.to eq([public_project, internal_project]) }
end
end
end end
end end
require 'spec_helper'
describe Gitlab::SQL::Union do
describe '#to_sql' do
it 'returns a String joining relations together using a UNION' do
rel1 = User.where(email: 'alice@example.com')
rel2 = User.where(email: 'bob@example.com')
union = described_class.new([rel1, rel2])
sql1 = rel1.reorder(nil).to_sql
sql2 = rel2.reorder(nil).to_sql
expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
end
end
end
...@@ -64,4 +64,42 @@ describe Event do ...@@ -64,4 +64,42 @@ describe Event do
it { expect(@event.branch_name).to eq("master") } it { expect(@event.branch_name).to eq("master") }
it { expect(@event.author).to eq(@user) } it { expect(@event.author).to eq(@user) }
end end
describe '.latest_update_time' do
describe 'when events are present' do
let(:time) { Time.utc(2015, 1, 1) }
before do
create(:closed_issue_event, updated_at: time)
create(:closed_issue_event, updated_at: time + 5)
end
it 'returns the latest update time' do
expect(Event.latest_update_time).to eq(time + 5)
end
end
describe 'when no events exist' do
it 'returns nil' do
expect(Event.latest_update_time).to be_nil
end
end
end
describe '.limit_recent' do
let!(:event1) { create(:closed_issue_event) }
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
subject { Event.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
subject { Event.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
end
end end
...@@ -38,6 +38,33 @@ describe Group do ...@@ -38,6 +38,33 @@ describe Group do
it { is_expected.not_to validate_presence_of :owner } it { is_expected.not_to validate_presence_of :owner }
end end
describe '.public_and_given_groups' do
let!(:public_group) { create(:group, public: true) }
subject { described_class.public_and_given_groups([group.id]) }
it { is_expected.to eq([public_group, group]) }
end
describe '.visible_to_user' do
let!(:group) { create(:group) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
describe 'when the user has access to a group' do
before do
group.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([group]) }
end
describe 'when the user does not have access to any groups' do
it { is_expected.to eq([]) }
end
end
describe '#to_reference' do describe '#to_reference' do
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}" expect(group.to_reference).to eq "@#{group.name}"
......
...@@ -464,4 +464,23 @@ describe Project do ...@@ -464,4 +464,23 @@ describe Project do
end end
end end
end end
describe '.visible_to_user' do
let!(:project) { create(:project, :private) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
describe 'when a user has access to a project' do
before do
project.team.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([project]) }
end
describe 'when a user does not have access to any projects' do
it { is_expected.to eq([]) }
end
end
end end
...@@ -686,7 +686,7 @@ describe User do ...@@ -686,7 +686,7 @@ describe User do
end end
end end
describe "#contributed_projects_ids" do describe "#contributed_projects" do
subject { create(:user) } subject { create(:user) }
let!(:project1) { create(:project) } let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project3) } let!(:project2) { create(:project, forked_from_project: project3) }
...@@ -701,15 +701,15 @@ describe User do ...@@ -701,15 +701,15 @@ describe User do
end end
it "includes IDs for projects the user has pushed to" do it "includes IDs for projects the user has pushed to" do
expect(subject.contributed_projects_ids).to include(project1.id) expect(subject.contributed_projects).to include(project1)
end end
it "includes IDs for projects the user has had merge requests merged into" do it "includes IDs for projects the user has had merge requests merged into" do
expect(subject.contributed_projects_ids).to include(project3.id) expect(subject.contributed_projects).to include(project3)
end end
it "doesn't include IDs for unrelated projects" do it "doesn't include IDs for unrelated projects" do
expect(subject.contributed_projects_ids).not_to include(project2.id) expect(subject.contributed_projects).not_to include(project2)
end end
end end
...@@ -758,4 +758,30 @@ describe User do ...@@ -758,4 +758,30 @@ describe User do
expect(subject.recent_push).to eq(nil) expect(subject.recent_push).to eq(nil)
end end
end end
describe '#authorized_groups' do
let!(:user) { create(:user) }
let!(:private_group) { create(:group) }
before do
private_group.add_user(user, Gitlab::Access::MASTER)
end
subject { user.authorized_groups }
it { is_expected.to eq([private_group]) }
end
describe '#authorized_projects' do
let!(:user) { create(:user) }
let!(:private_project) { create(:project, :private) }
before do
private_project.team << [user, Gitlab::Access::MASTER]
end
subject { user.authorized_projects }
it { is_expected.to eq([private_project]) }
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment