Commit 120fac8d authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-03-02' into 'master'

CE upstream - 2018-03-02 21:24 UTC

See merge request gitlab-org/gitlab-ee!4816
parents 224c5a6a 6b3de78d
...@@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager { ...@@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager {
page, page,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
filteredSearchTokenKeys, filteredSearchTokenKeys,
}) { }) {
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
...@@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager { ...@@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager {
this.page = page; this.page = page;
this.groupsOnly = isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor; this.groupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.setupMapping(); this.setupMapping();
......
...@@ -22,11 +22,13 @@ export default class FilteredSearchManager { ...@@ -22,11 +22,13 @@ export default class FilteredSearchManager {
page, page,
isGroup = false, isGroup = false,
isGroupAncestor = false, isGroupAncestor = false,
isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
}) { }) {
this.isGroup = isGroup; this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor; this.isGroupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.states = ['opened', 'closed', 'merged', 'all']; this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page; this.page = page;
......
import _ from 'underscore'; import _ from 'underscore';
import AjaxCache from '../lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import { objectToQueryString } from '~/lib/utils/common_utils';
import Flash from '../flash'; import Flash from '../flash';
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache'; import UsersCache from '../lib/utils/users_cache';
...@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens { ...@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens {
}; };
} }
/**
* Returns a computed API endpoint
* and query string composed of values from endpointQueryParams
* @param {String} endpoint
* @param {String} endpointQueryParams
*/
static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
if (!endpointQueryParams) {
return endpoint;
}
const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
return `${endpoint}?${queryString}`;
}
static unselectTokens() { static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
...@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens { ...@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) { static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`; const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
);
return AjaxCache.retrieve(labelsEndpoint) return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
......
...@@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => { ...@@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => {
}, {}); }, {});
}; };
/**
* Converts object with key-value pairs
* into query-param string
*
* @param {Object} params
*/
export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/** /**
......
...@@ -39,8 +39,11 @@ import 'ee/main'; ...@@ -39,8 +39,11 @@ import 'ee/main';
import initDispatcher from './dispatcher'; import initDispatcher from './dispatcher';
// eslint-disable-next-line global-require, import/no-commonjs // inject test utilities if necessary
if (process.env.NODE_ENV !== 'production') require('./test_utils/'); if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
import(/* webpackMode: "eager" */ './test_utils/');
}
svg4everybody(); svg4everybody();
......
...@@ -5,6 +5,7 @@ export default ({ ...@@ -5,6 +5,7 @@ export default ({
filteredSearchTokenKeys, filteredSearchTokenKeys,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
stateFiltersSelector, stateFiltersSelector,
}) => { }) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
...@@ -13,6 +14,7 @@ export default ({ ...@@ -13,6 +14,7 @@ export default ({
page, page,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
filteredSearchTokenKeys, filteredSearchTokenKeys,
stateFiltersSelector, stateFiltersSelector,
}); });
......
...@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder ...@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder
.public_or_visible_to_user(current_user) .public_or_visible_to_user(current_user)
end end
# Returns a collection of projects that is either public or visible to the
# logged in user.
#
# A caller must pass in a block to modify individual parts of
# the query, e.g. to apply .with_feature_available_for_user on top of it.
# This is useful for performance as we can stick those additional filters
# at the bottom of e.g. the UNION.
def projects_for_user
return yield(Project.public_to_user) unless current_user
# If the current_user is allowed to see all projects,
# we can shortcut and just return.
return yield(Project.all) if current_user.full_private_access?
authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
visible_projects = yield(Project.where(visibility_level: levels))
# We use a UNION here instead of OR clauses since this results in better
# performance.
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
Project.from("(#{union.to_sql}) AS #{Project.table_name}")
end
def feature_available_projects def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project # Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user) part.with_feature_available_for_user(:snippets, current_user)
end.select(:id) end.select(:id)
......
...@@ -322,42 +322,13 @@ class Project < ActiveRecord::Base ...@@ -322,42 +322,13 @@ class Project < ActiveRecord::Base
# Returns a collection of projects that is either public or visible to the # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
# def self.public_or_visible_to_user(user = nil)
# A caller may pass in a block to modify individual parts of if user
# the query, e.g. to apply .with_feature_available_for_user on top of it. where('EXISTS (?) OR projects.visibility_level IN (?)',
# This is useful for performance as we can stick those additional filters user.authorizations_for_projects,
# at the bottom of e.g. the UNION. Gitlab::VisibilityLevel.levels_for_user(user))
#
# Optionally, turning `use_where_in` off leads to returning a
# relation using #from instead of #where. This can perform much better
# but leads to trouble when used in conjunction with AR's #merge method.
def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
# If we don't get a block passed, use identity to avoid if/else repetitions
block = ->(part) { part } unless block_given?
return block.call(public_to_user) unless user
# If the user is allowed to see all projects,
# we can shortcut and just return.
return block.call(all) if user.full_private_access?
authorized = user
.project_authorizations
.select(1)
.where('project_authorizations.project_id = projects.id')
authorized_projects = block.call(where('EXISTS (?)', authorized))
levels = Gitlab::VisibilityLevel.levels_for_user(user)
visible_projects = block.call(where(visibility_level: levels))
# We use a UNION here instead of OR clauses since this results in better
# performance.
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
if use_where_in
where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
else else
from("(#{union.to_sql}) AS #{table_name}") public_to_user
end end
end end
...@@ -376,14 +347,11 @@ class Project < ActiveRecord::Base ...@@ -376,14 +347,11 @@ class Project < ActiveRecord::Base
elsif user elsif user
column = ProjectFeature.quoted_access_level_column(feature) column = ProjectFeature.quoted_access_level_column(feature)
authorized = user.project_authorizations.select(1)
.where('project_authorizations.project_id = projects.id')
with_project_feature with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible, visible,
ProjectFeature::PRIVATE, ProjectFeature::PRIVATE,
authorized) user.authorizations_for_projects)
else else
with_feature_access_level(feature, visible) with_feature_access_level(feature, visible)
end end
......
...@@ -99,7 +99,7 @@ class ChatNotificationService < Service ...@@ -99,7 +99,7 @@ class ChatNotificationService < Service
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
ChatMessage::PushMessage.new(data) ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue" when "issue"
ChatMessage::IssueMessage.new(data) unless update?(data) ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request" when "merge_request"
...@@ -145,10 +145,16 @@ class ChatNotificationService < Service ...@@ -145,10 +145,16 @@ class ChatNotificationService < Service
end end
def notify_for_ref?(data) def notify_for_ref?(data)
return true if data[:object_attributes][:tag] return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch? return true unless notify_only_default_branch?
data[:object_attributes][:ref] == project.default_branch ref = if data[:ref]
Gitlab::Git.ref_name(data[:ref])
else
data.dig(:object_attributes, :ref)
end
ref == project.default_branch
end end
def notify_for_pipeline?(data) def notify_for_pipeline?(data)
......
...@@ -619,6 +619,15 @@ class User < ActiveRecord::Base ...@@ -619,6 +619,15 @@ class User < ActiveRecord::Base
authorized_projects(min_access_level).exists?({ id: project.id }) authorized_projects(min_access_level).exists?({ id: project.id })
end end
# Typically used in conjunction with projects table to get projects
# a user has been given access to.
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
def authorizations_for_projects
project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
end
# Returns the projects this user has reporter (or greater) access to, limited # Returns the projects this user has reporter (or greater) access to, limited
# to at most the given projects. # to at most the given projects.
# #
......
...@@ -706,15 +706,15 @@ ...@@ -706,15 +706,15 @@
.checkbox .checkbox
= f.label :usage_ping_enabled do = f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured = f.check_box :usage_ping_enabled, disabled: !can_be_configured
Usage ping enabled Enable usage ping
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
.help-block .help-block
- if can_be_configured - if can_be_configured
Every week GitLab will report license usage back to GitLab, Inc. To help improve GitLab and its user experience, GitLab will
Disable this option if you do not want this to occur. To see the periodically collect usage information.
JSON payload that will be sent, visit the = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
= succeed '.' do about what information is shared with GitLab Inc. Visit
= link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping') = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
- else - else
The usage ping is disabled, and cannot be configured through this The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on form. For more information, see the documentation on
......
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
= webpack_bundle_tag "common" = webpack_bundle_tag "common"
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts) - if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts = yield :page_specific_javascripts
......
---
title: Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes
merge_request: 17345
author:
type: fixed
---
title: Revert Project.public_or_visible_to_user changes and only apply to snippets
merge_request:
author:
type: fixed
...@@ -54,7 +54,6 @@ function generateEntries() { ...@@ -54,7 +54,6 @@ function generateEntries() {
main: './main.js', main: './main.js',
ide: './ide/index.js', ide: './ide/index.js',
raven: './raven/index.js', raven: './raven/index.js',
test: './test.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
// EE-only // EE-only
......
...@@ -312,8 +312,7 @@ Parameters: ...@@ -312,8 +312,7 @@ Parameters:
"human_time_estimate": null, "human_time_estimate": null,
"human_total_time_spent": null "human_total_time_spent": null
}, },
"approvals_before_merge": null "approvals_before_merge": null,
},
"closed_at": "2018-01-19T14:36:11.086Z", "closed_at": "2018-01-19T14:36:11.086Z",
"latest_build_started_at": null, "latest_build_started_at": null,
"latest_build_finished_at": null, "latest_build_finished_at": null,
......
...@@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => {
}); });
}); });
describe('getEndpointWithQueryParams', () => {
it('returns `endpoint` string as is when second param `endpointQueryParams` is undefined, null or empty string', () => {
const endpoint = 'foo/bar/labels.json';
expect(subject.getEndpointWithQueryParams(endpoint)).toBe(endpoint);
expect(subject.getEndpointWithQueryParams(endpoint, null)).toBe(endpoint);
expect(subject.getEndpointWithQueryParams(endpoint, '')).toBe(endpoint);
});
it('returns `endpoint` string with values of `endpointQueryParams`', () => {
const endpoint = 'foo/bar/labels.json';
const singleQueryParams = '{"foo":"true"}';
const multipleQueryParams = '{"foo":"true","bar":"true"}';
expect(subject.getEndpointWithQueryParams(endpoint, singleQueryParams)).toBe(`${endpoint}?foo=true`);
expect(subject.getEndpointWithQueryParams(endpoint, multipleQueryParams)).toBe(`${endpoint}?foo=true&bar=true`);
});
});
describe('unselectTokens', () => { describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => { it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML; const beforeHTML = tokensContainer.innerHTML;
......
...@@ -166,6 +166,21 @@ describe('common_utils', () => { ...@@ -166,6 +166,21 @@ describe('common_utils', () => {
}); });
}); });
describe('objectToQueryString', () => {
it('returns empty string when `param` is undefined, null or empty string', () => {
expect(commonUtils.objectToQueryString()).toBe('');
expect(commonUtils.objectToQueryString('')).toBe('');
});
it('returns query string with values of `params`', () => {
const singleQueryParams = { foo: true };
const multipleQueryParams = { foo: true, bar: true };
expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true');
expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true');
});
});
describe('buildUrlWithCurrentLocation', () => { describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => { it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
......
...@@ -1684,6 +1684,32 @@ describe User do ...@@ -1684,6 +1684,32 @@ describe User do
end end
end end
describe '#authorizations_for_projects' do
let!(:user) { create(:user) }
subject { Project.where("EXISTS (?)", user.authorizations_for_projects) }
it 'includes projects that belong to a user, but no other projects' do
owned = create(:project, :private, namespace: user.namespace)
member = create(:project, :private).tap { |p| p.add_master(user) }
other = create(:project)
expect(subject).to include(owned)
expect(subject).to include(member)
expect(subject).not_to include(other)
end
it 'includes projects a user has access to, but no other projects' do
other_user = create(:user)
accessible = create(:project, :private, namespace: other_user.namespace) do |project|
project.add_developer(user)
end
other = create(:project)
expect(subject).to include(accessible)
expect(subject).not_to include(other)
end
end
describe '#authorized_projects', :delete do describe '#authorized_projects', :delete do
context 'with a minimum access level' do context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do it 'includes projects for which the user is an owner' do
......
...@@ -337,6 +337,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -337,6 +337,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
before do before do
chat_service.notify_only_default_branch = true chat_service.notify_only_default_branch = true
WebMock.stub_request(:post, webhook_url)
end end
it 'does not call the Slack/Mattermost API for pipeline events' do it 'does not call the Slack/Mattermost API for pipeline events' do
...@@ -345,6 +346,23 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -345,6 +346,23 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy expect(result).to be_falsy
end end
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
chat_service.execute(push_sample_data)
expect(WebMock).not_to have_requested(:post, webhook_url)
end
it 'notifies about push events for the default branch' do
push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end end
context 'when disabled' do context 'when disabled' do
......
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