Commit 783720dc authored by Ruben Davila's avatar Ruben Davila

Merge branch '8-12-stable-ee' into 8-12-stable-ee-to-master

Conflicts:
	VERSION
	db/schema.rb
	spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
parents 13659833 54ec1a6e
...@@ -6,6 +6,25 @@ v 8.13.0 (unreleased) ...@@ -6,6 +6,25 @@ v 8.13.0 (unreleased)
v 8.12.2 (unreleased) v 8.12.2 (unreleased)
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix prevent_secrets checkbox on admin view - Fix prevent_secrets checkbox on admin view
v 8.12.4 (unreleased)
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
- Fix errors importing project feature and milestone models using GitLab project import
- Make JWT messages Docker-compatible
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
- Fix duplicate branch entry in the merge request version compare dropdown
- Respect the fork_project permission when forking projects
- Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
......
...@@ -13,6 +13,7 @@ v 8.12.2 ...@@ -13,6 +13,7 @@ v 8.12.2
v 8.12.1 v 8.12.1
- Prevent secrets to be pushed to the repository - Prevent secrets to be pushed to the repository
- Prevent secrets to be pushed to the repository
v 8.12.0 v 8.12.0
- Include more data in EE usage ping - Include more data in EE usage ping
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json", namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json", groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true", projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels", labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
ldapGroupsPath: "/api/:version/ldap/:provider/groups.json", ldapGroupsPath: "/api/:version/ldap/:provider/groups.json",
...@@ -66,13 +66,14 @@ ...@@ -66,13 +66,14 @@
return callback(projects); return callback(projects);
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath) var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id); .replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({ return $.ajax({
url: url, url: url,
type: "POST", type: "POST",
data: data, data: {'label': data},
dataType: "json" dataType: "json"
}).done(function(label) { }).done(function(label) {
return callback(label); return callback(label);
......
...@@ -357,7 +357,7 @@ ...@@ -357,7 +357,7 @@
$('ul.emoji-menu-search, h5.emoji-search').remove(); $('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) { if (term) {
// Generate a search result block // Generate a search result block
h5 = $('<h5>').text('Search results'); h5 = $('<h5 class="emoji-search" />').text('Search results');
found_emojis = _this.searchEmojis(term).show(); found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide(); $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
...@@ -3,8 +3,7 @@ $(() => { ...@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function () {
const $this = $(this); const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
......
(function (w) { (function (w) {
class CreateLabelDropdown { class CreateLabelDropdown {
constructor ($el, projectId) { constructor ($el, namespacePath, projectPath) {
this.$el = $el; this.$el = $el;
this.projectId = projectId; this.namespacePath = namespacePath;
this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el); this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el); this.$newLabelField = $('#new_label_name', this.$el);
...@@ -91,8 +92,8 @@ ...@@ -91,8 +92,8 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Api.newLabel(this.projectId, { Api.newLabel(this.namespacePath, this.projectPath, {
name: this.$newLabelField.val(), title: this.$newLabelField.val(),
color: this.$newColorField.val() color: this.$newColorField.val()
}, (label) => { }, (label) => {
this.$newLabelCreateButton.enable(); this.$newLabelCreateButton.enable();
......
((w) => { ((w) => {
w.ResolveBtn = Vue.extend({ w.ResolveBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: { props: {
noteId: Number, noteId: Number,
discussionId: String, discussionId: String,
resolved: Boolean, resolved: Boolean,
namespacePath: String,
projectPath: String, projectPath: String,
canResolve: Boolean, canResolve: Boolean,
resolvedBy: String resolvedBy: String
...@@ -69,10 +65,10 @@ ...@@ -69,10 +65,10 @@
if (this.isResolved) { if (this.isResolved) {
promise = ResolveService promise = ResolveService
.unresolve(this.namespace, this.noteId); .unresolve(this.projectPath, this.noteId);
} else { } else {
promise = ResolveService promise = ResolveService
.resolve(this.namespace, this.noteId); .resolve(this.projectPath, this.noteId);
} }
promise.then((response) => { promise.then((response) => {
......
((w) => { ((w) => {
w.ResolveDiscussionBtn = Vue.extend({ w.ResolveDiscussionBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: { props: {
discussionId: String, discussionId: String,
mergeRequestId: Number, mergeRequestId: Number,
namespacePath: String,
projectPath: String, projectPath: String,
canResolve: Boolean, canResolve: Boolean,
}, },
...@@ -50,7 +46,7 @@ ...@@ -50,7 +46,7 @@
}, },
methods: { methods: {
resolve: function () { resolve: function () {
ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId); ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
} }
}, },
created: function () { created: function () {
......
((w) => {
w.ButtonMixins = {
computed: {
namespace: function () {
return `${this.namespacePath}/${this.projectPath}`;
}
}
};
})(window);
...@@ -9,32 +9,32 @@ ...@@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
} }
prepareRequest(namespace) { prepareRequest(root) {
this.setCSRF(); this.setCSRF();
Vue.http.options.root = `/${namespace}`; Vue.http.options.root = root;
} }
resolve(namespace, noteId) { resolve(projectPath, noteId) {
this.prepareRequest(namespace); this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {}); return this.noteResource.save({ noteId }, {});
} }
unresolve(namespace, noteId) { unresolve(projectPath, noteId) {
this.prepareRequest(namespace); this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {}); return this.noteResource.delete({ noteId }, {});
} }
toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) { toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId], const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved(); isResolved = discussion.isResolved();
let promise; let promise;
if (isResolved) { if (isResolved) {
promise = this.unResolveAll(namespace, mergeRequestId, discussionId); promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else { } else {
promise = this.resolveAll(namespace, mergeRequestId, discussionId); promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
} }
promise.then((response) => { promise.then((response) => {
...@@ -57,10 +57,10 @@ ...@@ -57,10 +57,10 @@
}) })
} }
resolveAll(namespace, mergeRequestId, discussionId) { resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace); this.prepareRequest(projectPath);
discussion.loading = true; discussion.loading = true;
...@@ -70,10 +70,10 @@ ...@@ -70,10 +70,10 @@
}, {}); }, {});
} }
unResolveAll(namespace, mergeRequestId, discussionId) { unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace); this.prepareRequest(projectPath);
discussion.loading = true; discussion.loading = true;
......
...@@ -4,9 +4,10 @@ ...@@ -4,9 +4,10 @@
var _this; var _this;
_this = this; _this = this;
$('.js-label-select').each(function(i, dropdown) { $('.js-label-select').each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip; var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels'); labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate'); issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected'); selectedLabel = $dropdown.data('selected');
...@@ -24,6 +25,11 @@ ...@@ -24,6 +25,11 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value'); $value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('field-name') + '"]')
.map(function () {
return this.value;
}).get();
if (issueUpdateURL != null) { if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/'); issueURLSplit = issueUpdateURL.split('/');
} }
...@@ -35,7 +41,7 @@ ...@@ -35,7 +41,7 @@
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
} }
saveLabelData = function() { saveLabelData = function() {
...@@ -43,6 +49,10 @@ ...@@ -43,6 +49,10 @@
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
return this.value; return this.value;
}).get(); }).get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
data = {}; data = {};
data[abilityName] = {}; data[abilityName] = {};
data[abilityName].label_ids = selected; data[abilityName].label_ids = selected;
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
while (i < sURLVariables.length) { while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('='); sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) { if (sParameterName[0] === sParam) {
values.push(sParameterName[1]); values.push(sParameterName[1].replace(/\+/g, ' '));
} }
i++; i++;
} }
......
...@@ -432,14 +432,12 @@ ...@@ -432,14 +432,12 @@
var $form = $(xhr.target); var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) { if ($form.attr('data-resolve-all') != null) {
var namespacePath = $form.attr('data-namespace-path'), var projectPath = $form.data('project-path')
projectPath = $form.attr('data-project-path') discussionId = $form.data('discussion-id'),
discussionId = $form.attr('data-discussion-id'), mergeRequestId = $form.data('noteable-iid');
mergeRequestId = $form.attr('data-noteable-iid'),
namespace = namespacePath + '/' + projectPath;
if (ResolveService != null) { if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId); ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
} }
} }
...@@ -854,7 +852,6 @@ ...@@ -854,7 +852,6 @@
.closest('form') .closest('form')
.attr('data-discussion-id', discussionId) .attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true') .attr('data-resolve-all', 'true')
.attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path')); .attr('data-project-path', $this.attr('data-project-path'));
}; };
......
...@@ -25,7 +25,7 @@ class JwtController < ApplicationController ...@@ -25,7 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password| authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_403 unless @authentication_result.success? && render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end end
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalTokenError
...@@ -33,10 +33,21 @@ class JwtController < ApplicationController ...@@ -33,10 +33,21 @@ class JwtController < ApplicationController
end end
def render_missing_personal_token def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \ render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}", "You can generate one at #{profile_personal_access_tokens_url}" }
status: 401 ] }, status: 401
end
def render_unauthorized
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
] }, status: 401
end end
def auth_params def auth_params
......
...@@ -60,7 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -60,7 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def show def show
raw_notes = @issue.notes_with_associations.fresh raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer. @notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref) render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
......
...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params) @label = @project.labels.create(label_params)
if @label.valid? if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project) respond_to do |format|
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
format.json { render json: @label }
end
else else
render 'new' respond_to do |format|
format.html { render 'new' }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end end
end end
......
...@@ -65,7 +65,7 @@ class UsersController < ApplicationController ...@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' } format.html { render 'show' }
format.json do format.json do
render json: { render json: {
html: view_to_html_string("snippets/_snippets", collection: @snippets) html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
} }
end end
end end
......
...@@ -110,7 +110,7 @@ class Notify < BaseMailer ...@@ -110,7 +110,7 @@ class Notify < BaseMailer
headers['X-GitLab-Reply-Key'] = reply_key headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable? if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true) headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end end
......
...@@ -147,9 +147,13 @@ module Elastic ...@@ -147,9 +147,13 @@ module Elastic
} }
end end
def project_ids_filter(query_hash, project_ids, public_and_internal_projects = true) def project_ids_filter(query_hash, options)
if project_ids if options[:project_ids]
condition = project_ids_condition(project_ids, public_and_internal_projects) condition = project_ids_condition(
options[:current_user],
options[:project_ids],
options[:public_and_internal_projects]
)
query_hash[:query][:bool][:filter] = { query_hash[:query][:bool][:filter] = {
has_parent: { has_parent: {
...@@ -166,19 +170,17 @@ module Elastic ...@@ -166,19 +170,17 @@ module Elastic
query_hash query_hash
end end
def project_ids_condition(project_ids, public_and_internal_projects) def project_ids_condition(current_user, project_ids, public_and_internal_projects)
conditions = [{ conditions = [{
terms: { id: project_ids } terms: { id: project_ids }
}] }]
if public_and_internal_projects if public_and_internal_projects
conditions << { conditions << { term: { visibility_level: Project::PUBLIC } }
term: { visibility_level: Project::PUBLIC }
}
conditions << { if current_user
term: { visibility_level: Project::INTERNAL } conditions << { term: { visibility_level: Project::INTERNAL } }
} end
end end
conditions conditions
......
...@@ -44,7 +44,7 @@ module Elastic ...@@ -44,7 +44,7 @@ module Elastic
query_hash = basic_query_hash(%w(title^2 description), query) query_hash = basic_query_hash(%w(title^2 description), query)
end end
query_hash = project_ids_filter(query_hash, options[:project_ids], options[:public_and_internal_projects]) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user]) query_hash = confidentiality_filter(query_hash, options[:current_user])
self.__elasticsearch__.search(query_hash) self.__elasticsearch__.search(query_hash)
......
...@@ -66,7 +66,7 @@ module Elastic ...@@ -66,7 +66,7 @@ module Elastic
query_hash = basic_query_hash(%w(title^2 description), query) query_hash = basic_query_hash(%w(title^2 description), query)
end end
query_hash = project_ids_filter(query_hash, options[:project_ids], options[:public_and_internal_projects]) query_hash = project_ids_filter(query_hash, options)
self.__elasticsearch__.search(query_hash) self.__elasticsearch__.search(query_hash)
end end
......
...@@ -31,7 +31,7 @@ module Elastic ...@@ -31,7 +31,7 @@ module Elastic
query_hash = basic_query_hash(options[:in], query) query_hash = basic_query_hash(options[:in], query)
query_hash = project_ids_filter(query_hash, options[:project_ids], options[:public_and_internal_projects]) query_hash = project_ids_filter(query_hash, options)
self.__elasticsearch__.search(query_hash) self.__elasticsearch__.search(query_hash)
end end
......
...@@ -60,7 +60,7 @@ module Elastic ...@@ -60,7 +60,7 @@ module Elastic
query_hash[:track_scores] = true query_hash[:track_scores] = true
end end
query_hash = project_ids_filter(query_hash, options[:project_ids], options[:public_and_internal_projects]) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user]) query_hash = confidentiality_filter(query_hash, options[:current_user])
query_hash[:sort] = [ query_hash[:sort] = [
......
...@@ -83,10 +83,10 @@ module Elastic ...@@ -83,10 +83,10 @@ module Elastic
} }
end end
if options[:pids] if options[:project_ids]
filters << { filters << {
bool: { bool: {
should: project_ids_condition(options[:pids], options[:public_and_internal_projects]) should: project_ids_condition(options[:current_user], options[:project_ids], options[:public_and_internal_projects])
} }
} }
end end
......
...@@ -10,15 +10,33 @@ class CycleAnalytics ...@@ -10,15 +10,33 @@ class CycleAnalytics
end end
def commits def commits
repository = @project.repository.raw_repository ref = @project.default_branch.presence
count_commits_for(ref)
if @project.default_branch
repository.log(ref: @project.default_branch, after: @from).count
end
end end
def deploys def deploys
@project.deployments.where("created_at > ?", @from).count @project.deployments.where("created_at > ?", @from).count
end end
private
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
def count_commits_for(ref)
return unless ref
repository = @project.repository.raw_repository
sha = @project.repository.commit(ref).sha
cmd = %W(git --git-dir=#{repository.path} log)
cmd << '--format=%H'
cmd << "--after=#{@from.iso8601}"
cmd << sha
raw_output = IO.popen(cmd) { |io| io.read }
raw_output.lines.count
end
end end
end end
...@@ -7,10 +7,10 @@ module Auth ...@@ -7,10 +7,10 @@ module Auth
def execute(authentication_abilities:) def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
return error('not found', 404) unless registry.enabled return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project unless current_user || project
return error('forbidden', 403) unless scope return error('DENIED', status: 403, message: 'access forbidden') unless scope
end end
{ token: authorized_token(scope).encoded } { token: authorized_token(scope).encoded }
...@@ -111,5 +111,12 @@ module Auth ...@@ -111,5 +111,12 @@ module Auth
@authentication_abilities.include?(:create_container_image) && @authentication_abilities.include?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project) can?(current_user, :create_container_image, requested_project)
end end
def error(code, status:, message: '')
{
errors: [{ code: code, message: message }],
http_status: status
}
end
end end
end end
...@@ -15,6 +15,11 @@ module Projects ...@@ -15,6 +15,11 @@ module Projects
return @project return @project
end end
unless allowed_fork?(forked_from_project_id)
@project.errors.add(:forked_from_project_id, 'is forbidden')
return @project
end
# Set project name from path # Set project name from path
if @project.name.present? && @project.path.present? if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok # if both name and path set - everything is ok
...@@ -71,6 +76,13 @@ module Projects ...@@ -71,6 +76,13 @@ module Projects
@project.errors.add(:namespace, "is not valid") @project.errors.add(:namespace, "is not valid")
end end
def allowed_fork?(source_project_id)
return true if source_project_id.nil?
source_project = Project.find_by(id: source_project_id)
current_user.can?(:fork_project, source_project)
end
def allowed_namespace?(user, namespace_id) def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id) namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, namespace)
......
...@@ -16,6 +16,8 @@ module Projects ...@@ -16,6 +16,8 @@ module Projects
end end
new_project = CreateService.new(current_user, new_params).execute new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
......
...@@ -16,8 +16,7 @@ ...@@ -16,8 +16,7 @@
%tr %tr
%td #{stage.capitalize} Job - #{build[:name]} %td #{stage.capitalize} Job - #{build[:name]}
%td %td
%pre %pre= build[:commands]
= simple_format build[:commands]
%br %br
%b Tag list: %b Tag list:
......
- if discussion.for_merge_request? - if discussion.for_merge_request?
%resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'", %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
":project-path" => "'#{discussion.project.path}'",
":discussion-id" => "'#{discussion.id}'", ":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid, ":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user), ":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true } "inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" } .btn-group{ role: "group", "v-if" => "showButton" }
%button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" } %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
= icon("spinner spin", "v-show" => "loading") = icon("spinner spin", "v-show" => "loading")
{{ buttonText }} {{ buttonText }}
...@@ -8,13 +8,7 @@ ...@@ -8,13 +8,7 @@
%tbody %tbody
%th Status %th Status
%th Commit %th Commit
- pipelines.stages.each do |stage| %th Stages
%th.stage
- if stage.titleize.length > 12
%span.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize
- else
= stage.titleize
%th %th
%th %th
= render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- if @merge_request.reopenable? - if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" } %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } } %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }} {{ buttonText }}
#notes= render "projects/notes/notes_with_form" #notes= render "projects/notes/notes_with_form"
...@@ -24,14 +24,12 @@ ...@@ -24,14 +24,12 @@
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => "#{project_path(note.project)}",
%resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'", "discussion-id" => "#{note.discussion_id}",
":project-path" => "'#{note.project.path}'",
":discussion-id" => "'#{note.discussion_id}'",
":note-id" => note.id, ":note-id" => note.id,
":resolved" => note.resolved?, ":resolved" => note.resolved?,
":can-resolve" => can_resolve, ":can-resolve" => can_resolve,
":resolved-by" => "'#{note.resolved_by.try(:name)}'", "resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}", "v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true, "inline-template" => true,
"v-ref:note_#{note.id}" => true } "v-ref:note_#{note.id}" => true }
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
.dropdown.pull-right .dropdown.pull-right
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Create new list Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- show_footer = local_assigns.fetch(:show_footer, true) - show_footer = local_assigns.fetch(:show_footer, true)
- data_options = local_assigns.fetch(:data_options, {}) - data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, []) - classes = local_assigns.fetch(:classes, [])
- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} - dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Label"}
- dropdown_data.merge!(data_options) - dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options - classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit - classes << 'js-filter-submit' if filter_submit
......
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
- issuable.labels_array.each do |label| - issuable.labels_array.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%span.dropdown-toggle-text %span.dropdown-toggle-text
Label Label
= icon('chevron-down') = icon('chevron-down')
......
- remote = local_assigns.fetch(:remote, false)
.snippets-list-holder .snippets-list-holder
%ul.content-list %ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets = render partial: 'shared/snippets/snippet', collection: @snippets
...@@ -5,7 +7,7 @@ ...@@ -5,7 +7,7 @@
%li %li
.nothing-here-block Nothing here. .nothing-here-block Nothing here.
= paginate @snippets, theme: 'gitlab', remote: true = paginate @snippets, theme: 'gitlab', remote: remote
:javascript :javascript
gl.SnippetsList(); gl.SnippetsList();
...@@ -102,13 +102,24 @@ module Gitlab ...@@ -102,13 +102,24 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb) config.action_view.sanitized_allowed_protocols = %w(smb)
config.middleware.use Rack::Attack config.middleware.insert_before Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains # Allow access to GitLab API from other domains
config.middleware.use Rack::Cors do config.middleware.insert_before Warden::Manager, Rack::Cors do
allow do
origins Gitlab.config.gitlab.url
resource '/api/*',
credentials: true,
headers: :any,
methods: :any,
expose: ['Link']
end
# Cross-origin requests must not have the session cookie available
allow do allow do
origins '*' origins '*'
resource '/api/*', resource '/api/*',
credentials: false,
headers: :any, headers: :any,
methods: :any, methods: :any,
expose: ['Link'] expose: ['Link']
......
...@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike') user = create(:user, name: 'Mike')
@project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute @forked_project = Projects::ForkService.new(@project, user).execute
end end
......
...@@ -21,8 +21,11 @@ module API ...@@ -21,8 +21,11 @@ module API
end end
# Check the Rails session for valid authentication details # Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden def find_user_from_warden
warden ? warden.authenticate : nil warden.try(:authenticate) if request.get? || request.head?
end end
def find_user_by_private_token def find_user_by_private_token
......
...@@ -90,7 +90,7 @@ module API ...@@ -90,7 +90,7 @@ module API
{ {
username: token_handler.actor_name, username: token_handler.actor_name,
lfs_token: token_handler.generate, lfs_token: token_handler.token,
repository_http_path: project.http_url_to_repo repository_http_path: project.http_url_to_repo
} }
end end
......
...@@ -129,7 +129,7 @@ module Gitlab ...@@ -129,7 +129,7 @@ module Gitlab
read_authentication_abilities read_authentication_abilities
end end
Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.value, password) Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
end end
def build_access_token_check(login, password) def build_access_token_check(login, password)
......
module Gitlab module Gitlab
module Elastic module Elastic
class SearchResults class SearchResults
attr_reader :current_user, :query attr_reader :current_user, :query, :public_and_internal_projects
# Limit search results by passed project ids # Limit search results by passed project ids
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -59,41 +59,28 @@ module Gitlab ...@@ -59,41 +59,28 @@ module Gitlab
private private
def projects def base_options
opt = { {
pids: limit_project_ids, current_user: current_user,
public_and_internal_projects: @public_and_internal_projects project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects
} }
end
@projects = Project.elastic_search(query, options: opt) def projects
Project.elastic_search(query, options: base_options)
end end
def issues def issues
opt = { Issue.elastic_search(query, options: base_options)
project_ids: limit_project_ids,
current_user: current_user,
public_and_internal_projects: @public_and_internal_projects
}
Issue.elastic_search(query, options: opt)
end end
def milestones def milestones
opt = { Milestone.elastic_search(query, options: base_options)
project_ids: limit_project_ids,
public_and_internal_projects: @public_and_internal_projects
}
Milestone.elastic_search(query, options: opt)
end end
def merge_requests def merge_requests
opt = { MergeRequest.elastic_search(query, options: base_options)
project_ids: limit_project_ids,
public_and_internal_projects: @public_and_internal_projects
}
MergeRequest.elastic_search(query, options: opt)
end end
def blobs def blobs
...@@ -101,7 +88,7 @@ module Gitlab ...@@ -101,7 +88,7 @@ module Gitlab
Kaminari.paginate_array([]) Kaminari.paginate_array([])
else else
opt = { opt = {
additional_filter: build_filter_by_project(limit_project_ids, @public_and_internal_projects) additional_filter: build_filter_by_project
} }
Repository.search( Repository.search(
...@@ -117,7 +104,7 @@ module Gitlab ...@@ -117,7 +104,7 @@ module Gitlab
Kaminari.paginate_array([]) Kaminari.paginate_array([])
else else
options = { options = {
additional_filter: build_filter_by_project(limit_project_ids, @public_and_internal_projects) additional_filter: build_filter_by_project
} }
Repository.find_commits_by_message_with_elastic( Repository.find_commits_by_message_with_elastic(
...@@ -129,17 +116,15 @@ module Gitlab ...@@ -129,17 +116,15 @@ module Gitlab
end end
end end
def build_filter_by_project(project_ids, public_and_internal_projects) def build_filter_by_project
conditions = [{ terms: { id: project_ids } }] conditions = [{ terms: { id: limit_project_ids } }]
if public_and_internal_projects if public_and_internal_projects
conditions << { conditions << { term: { visibility_level: Project::PUBLIC } }
term: { visibility_level: Project::PUBLIC }
}
conditions << { if current_user
term: { visibility_level: Project::INTERNAL } conditions << { term: { visibility_level: Project::INTERNAL } }
} end
end end
{ {
......
# Model relationships to be included in the project import/export # Model relationships to be included in the project import/export
project_tree: project_tree:
- :labels
- milestones:
- :events
- issues: - issues:
- :events - :events
- notes: - notes:
...@@ -39,9 +42,6 @@ project_tree: ...@@ -39,9 +42,6 @@ project_tree:
- protected_branches: - protected_branches:
- :merge_access_levels - :merge_access_levels
- :push_access_levels - :push_access_levels
- :labels
- milestones:
- :events
- :project_feature - :project_feature
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
......
...@@ -61,11 +61,17 @@ module Gitlab ...@@ -61,11 +61,17 @@ module Gitlab
def restore_project def restore_project
return @project unless @tree_hash return @project unless @tree_hash
project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
@project.update(project_params) @project.update(project_params)
@project @project
end end
def project_params
@tree_hash.reject do |key, value|
# return params that are not 1 to many or 1 to 1 relations
value.is_a?(Array) || key == key.singularize
end
end
# Given a relation hash containing one or more models and its relationships, # Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and # loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+ # and assigns its correspondent attributes hash from +tree_hash+
......
...@@ -17,20 +17,19 @@ module Gitlab ...@@ -17,20 +17,19 @@ module Gitlab
end end
end end
def generate def token
token = Devise.friendly_token(TOKEN_LENGTH)
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
token = redis.get(redis_key)
if token
redis.expire(redis_key, EXPIRY_TIME)
else
token = Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME) redis.set(redis_key, token, ex: EXPIRY_TIME)
end end
token token
end end
def value
Gitlab::Redis.with do |redis|
redis.get(redis_key)
end
end end
def user? def user?
......
...@@ -73,8 +73,8 @@ describe UsersController do ...@@ -73,8 +73,8 @@ describe UsersController do
end end
context 'forked project' do context 'forked project' do
let!(:project) { create(:project) } let(:project) { create(:project) }
let!(:forked_project) { Projects::ForkService.new(project, user).execute } let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do before do
sign_in(user) sign_in(user)
......
...@@ -62,6 +62,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -62,6 +62,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:bug) { create(:label, project: project, name: 'Bug') } let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') } let!(:backlog) { create(:label, project: project, name: 'Backlog') }
let!(:done) { create(:label, project: project, name: 'Done') } let!(:done) { create(:label, project: project, name: 'Done') }
let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:list1) { create(:list, board: project.board, label: planning, position: 0) } let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
let!(:list2) { create(:list, board: project.board, label: development, position: 1) } let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
...@@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) } let!(:issue8) { create(:closed_issue, project: project) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) } let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do before do
visit namespace_project_board_path(project.namespace, project) visit namespace_project_board_path(project.namespace, project)
...@@ -441,6 +442,34 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -441,6 +442,34 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..4))
end end
it 'filters by label with space after reload' do
page.within '.issues-filters' do
click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link(accepting.title)
wait_for_vue_resource(spinner: false)
find('.dropdown-menu-close').click
end
end
# Test after reload
page.evaluate_script 'window.location.reload()'
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end
it 'infinite scrolls list with label filter' do it 'infinite scrolls list with label filter' do
50.times do 50.times do
create(:labeled_issue, project: project, labels: [testing]) create(:labeled_issue, project: project, labels: [testing])
......
require 'spec_helper'
describe 'Dashboard snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
login_as(project.owner)
visit dashboard_snippets_path
end
it_behaves_like 'paginated snippets'
end
end
...@@ -369,6 +369,24 @@ describe 'Issues', feature: true do ...@@ -369,6 +369,24 @@ describe 'Issues', feature: true do
end end
end end
describe 'update labels from issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let!(:label) { create(:label, project: project) }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'will not send ajax request when no data is changed' do
page.within '.labels' do
click_link 'Edit'
first('.dropdown-menu-close').click
expect(page).not_to have_selector('.block-loading')
end
end
end
describe 'update assignee from issue#show' do describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
......
require 'spec_helper'
describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit namespace_project_snippets_path(project.namespace, project)
end
it_behaves_like 'paginated snippets'
end
end
require 'spec_helper'
describe 'Snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit snippets_path(username: project.owner.username)
end
it_behaves_like 'paginated snippets'
end
end
...@@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do ...@@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do
let(:mail) { ActionMailer::Base.deliveries.last } let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) } let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
let(:header_link) { mail.header['List-Unsubscribe'] } let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
let(:body_link) { body.find_link('unsubscribe')['href'] } let(:body_link) { body.find_link('unsubscribe')['href'] }
before do before do
......
...@@ -3,30 +3,16 @@ require 'spec_helper' ...@@ -3,30 +3,16 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do describe 'Snippets tab on a user profile', feature: true, js: true do
include WaitForAjax include WaitForAjax
let(:user) { create(:user) }
context 'when the user has snippets' do context 'when the user has snippets' do
let(:user) { create(:user) }
let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
before do before do
create_list(:snippet, 25, :public, author: user) allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user) visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' } page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax wait_for_ajax
end end
it 'is limited to 20 items per page' do it_behaves_like 'paginated snippets', remote: true
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20)
end
context 'clicking on the link to the second page' do
before do
click_link('2')
wait_for_ajax
end
it 'shows the remaining snippets' do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5)
end
end
end end
end end
...@@ -11,7 +11,7 @@ describe ProjectsHelper do ...@@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do describe "can_change_visibility_level?" do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute } let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do it "returns false if there are no appropriate permissions" do
......
...@@ -48,9 +48,9 @@ ...@@ -48,9 +48,9 @@
setTimeout(() => { setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10); expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => { $('.dropdown-content a').each(function (i) {
if (i < 5) { if (i < saveLabelCount) {
$link.get(0).click(); $(this).get(0).click();
} }
}); });
...@@ -70,9 +70,9 @@ ...@@ -70,9 +70,9 @@
setTimeout(() => { setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10); expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => { $('.dropdown-content a').each(function (i) {
if (i < 5) { if (i < saveLabelCount) {
$link.get(0).click(); $(this).get(0).click();
} }
}); });
...@@ -86,4 +86,3 @@ ...@@ -86,4 +86,3 @@
}); });
}); });
})(); })();
...@@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do ...@@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes user lfs tokens' do it 'recognizes user lfs tokens' do
user = create(:user) user = create(:user)
ip = 'ip' ip = 'ip'
token = Gitlab::LfsToken.new(user).generate token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
...@@ -73,7 +73,7 @@ describe Gitlab::Auth, lib: true do ...@@ -73,7 +73,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes deploy key lfs tokens' do it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key) key = create(:deploy_key)
ip = 'ip' ip = 'ip'
token = Gitlab::LfsToken.new(key).generate token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}") expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
......
...@@ -374,7 +374,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -374,7 +374,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
end end
it 'founds blobs' do it 'finds blobs' do
results = described_class.new(user, 'def', limit_project_ids) results = described_class.new(user, 'def', limit_project_ids)
blobs = results.objects('blobs') blobs = results.objects('blobs')
...@@ -382,7 +382,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -382,7 +382,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
expect(results.blobs_count).to eq 4 expect(results.blobs_count).to eq 4
end end
it 'founds blobs from public projects only' do it 'finds blobs from public projects only' do
project_2 = create :project, :private project_2 = create :project, :private
project_2.repository.index_blobs project_2.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
...@@ -408,7 +408,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -408,7 +408,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
end end
it 'founds commits' do it 'finds commits' do
results = described_class.new(user, 'add', limit_project_ids) results = described_class.new(user, 'add', limit_project_ids)
commits = results.objects('commits') commits = results.objects('commits')
...@@ -416,7 +416,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -416,7 +416,7 @@ describe Gitlab::Elastic::SearchResults, lib: true do
expect(results.commits_count).to eq 5 expect(results.commits_count).to eq 5
end end
it 'founds commits from public projects only' do it 'finds commits from public projects only' do
project_2 = create :project, :private project_2 = create :project, :private
project_2.repository.index_commits project_2.repository.index_commits
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
...@@ -434,4 +434,190 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -434,4 +434,190 @@ describe Gitlab::Elastic::SearchResults, lib: true do
expect(results.commits_count).to eq 0 expect(results.commits_count).to eq 0
end end
end end
describe 'Visibility levels' do
let(:internal_project) { create(:project, :internal, description: "Internal project") }
let(:private_project1) { create(:project, :private, description: "Private project") }
let(:private_project2) { create(:project, :private, description: "Private project where I'm a member") }
let(:public_project) { create(:project, :public, description: "Public project") }
let(:limit_project_ids) { [private_project2.id] }
before do
private_project2.project_members.create(user: user, access_level: ProjectMember::DEVELOPER)
end
context 'Issues' do
it 'finds right set of issues' do
issue_1 = create :issue, project: internal_project, title: "Internal project"
create :issue, project: private_project1, title: "Private project"
issue_3 = create :issue, project: private_project2, title: "Private project where I'm a member"
issue_4 = create :issue, project: public_project, title: "Public project"
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
issues = results.objects('issues')
expect(issues).to include issue_1
expect(issues).to include issue_3
expect(issues).to include issue_4
expect(results.issues_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'project', [])
issues = results.objects('issues')
expect(issues).to include issue_4
expect(results.issues_count).to eq 1
end
end
context 'Milestones' do
it 'finds right set of milestine' do
milestone_1 = create :milestone, project: internal_project, title: "Internal project"
create :milestone, project: private_project1, title: "Private project"
milestone_3 = create :milestone, project: private_project2, title: "Private project where I'm a member"
milestone_4 = create :milestone, project: public_project, title: "Public project"
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
milestones = results.objects('milestones')
expect(milestones).to include milestone_1
expect(milestones).to include milestone_3
expect(milestones).to include milestone_4
expect(results.milestones_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'project', [])
milestones = results.objects('milestones')
expect(milestones).to include milestone_4
expect(results.milestones_count).to eq 1
end
end
context 'Projects' do
it 'finds right set of projects' do
internal_project
private_project1
private_project2
public_project
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
milestones = results.objects('projects')
expect(milestones).to include internal_project
expect(milestones).to include private_project2
expect(milestones).to include public_project
expect(results.projects_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'project', [])
projects = results.objects('projects')
expect(projects).to include public_project
expect(results.projects_count).to eq 1
end
end
context 'Merge Requests' do
it 'finds right set of merge requests' do
merge_request_1 = create :merge_request, target_project: internal_project, source_project: internal_project, title: "Internal project"
create :merge_request, target_project: private_project1, source_project: private_project1, title: "Private project"
merge_request_3 = create :merge_request, target_project: private_project2, source_project: private_project2, title: "Private project where I'm a member"
merge_request_4 = create :merge_request, target_project: public_project, source_project: public_project, title: "Public project"
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
merge_requests = results.objects('merge_requests')
expect(merge_requests).to include merge_request_1
expect(merge_requests).to include merge_request_3
expect(merge_requests).to include merge_request_4
expect(results.merge_requests_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'project', [])
merge_requests = results.objects('merge_requests')
expect(merge_requests).to include merge_request_4
expect(results.merge_requests_count).to eq 1
end
end
context 'Commits' do
it 'finds right set of commits' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.commit_file(
user,
'test-file',
'search test',
'search test',
'master',
false
)
project.repository.index_commits
end
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'search', limit_project_ids)
commits = results.objects('commits')
expect(commits.map(&:project)).to match_array [internal_project, private_project2, public_project]
expect(results.commits_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'search', [])
commits = results.objects('commits')
expect(commits.first.project).to eq public_project
expect(results.commits_count).to eq 1
end
end
context 'Blobs' do
it 'finds right set of blobs' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.commit_file(
user,
'test-file',
'tesla',
'search test',
'master',
false
)
project.repository.index_blobs
end
Gitlab::Elastic::Helper.refresh_index
# Authenticated search
results = described_class.new(user, 'tesla', limit_project_ids)
blobs = results.objects('blobs')
expect(blobs.map{ |blob| blob._parent.to_i }).to match_array [internal_project.id, private_project2.id, public_project.id]
expect(results.blobs_count).to eq 3
# Unauthenticated search
results = described_class.new(nil, 'tesla', [])
blobs = results.objects('blobs')
expect(blobs.first._parent.to_i).to eq public_project.id.to_i
expect(results.blobs_count).to eq 1
end
end
end
end end
...@@ -2231,6 +2231,31 @@ ...@@ -2231,6 +2231,31 @@
], ],
"milestones": [ "milestones": [
{
"id": 1,
"title": "test milestone",
"project_id": 8,
"description": "test milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"events": [
{
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
"action": 1,
"author_id": 18
}
]
},
{ {
"id": 20, "id": 20,
"title": "v4.0", "title": "v4.0",
...@@ -7373,5 +7398,16 @@ ...@@ -7373,5 +7398,16 @@
} }
] ]
} }
] ],
"project_feature": {
"builds_access_level": 0,
"created_at": "2014-12-26T09:26:45.000Z",
"id": 2,
"issues_access_level": 0,
"merge_requests_access_level": 20,
"project_id": 4,
"snippets_access_level": 20,
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
} }
\ No newline at end of file
...@@ -107,6 +107,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -107,6 +107,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil expect(Label.first.label_links.first.target).not_to be_nil
end end
it 'has a project feature' do
restored_project_json
expect(project.project_feature).not_to be_nil
end
it 'restores the correct service' do it 'restores the correct service' do
restored_project_json restored_project_json
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::LfsToken, lib: true do describe Gitlab::LfsToken, lib: true do
describe '#generate and #value' do describe '#token' do
shared_examples 'an LFS token generator' do shared_examples 'an LFS token generator' do
it 'returns a randomly generated token' do it 'returns a randomly generated token' do
token = handler.generate token = handler.token
expect(token).not_to be_nil expect(token).not_to be_nil
expect(token).to be_a String expect(token).to be_a String
...@@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do ...@@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do
end end
it 'returns the correct token based on the key' do it 'returns the correct token based on the key' do
token = handler.generate token = handler.token
expect(handler.value).to eq(token) expect(handler.token).to eq(token)
end end
end end
......
...@@ -169,8 +169,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do ...@@ -169,8 +169,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end end
shared_examples 'an unsubscribeable thread' do shared_examples 'an unsubscribeable thread' do
it 'has a List-Unsubscribe header' do it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
end end
it { is_expected.to have_body_text /unsubscribe/ } it { is_expected.to have_body_text /unsubscribe/ }
......
...@@ -24,9 +24,9 @@ describe Project, elastic: true do ...@@ -24,9 +24,9 @@ describe Project, elastic: true do
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
end end
expect(described_class.elastic_search('test', options: { pids: project_ids }).total_count).to eq(1) expect(described_class.elastic_search('test', options: { project_ids: project_ids }).total_count).to eq(1)
expect(described_class.elastic_search('test1', options: { pids: project_ids }).total_count).to eq(1) expect(described_class.elastic_search('test1', options: { project_ids: project_ids }).total_count).to eq(1)
expect(described_class.elastic_search('someone_elses_project', options: { pids: project_ids }).total_count).to eq(0) expect(described_class.elastic_search('someone_elses_project', options: { project_ids: project_ids }).total_count).to eq(0)
end end
it "finds partial matches in project names" do it "finds partial matches in project names" do
...@@ -40,7 +40,7 @@ describe Project, elastic: true do ...@@ -40,7 +40,7 @@ describe Project, elastic: true do
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
end end
expect(described_class.elastic_search('tesla', options: { pids: project_ids }).total_count).to eq(2) expect(described_class.elastic_search('tesla', options: { project_ids: project_ids }).total_count).to eq(2)
end end
it "returns json with all needed elements" do it "returns json with all needed elements" do
......
...@@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do ...@@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do
expect(subject.commits).to eq(0) expect(subject.commits).to eq(0)
end end
it "finds a large (> 100) snumber of commits if present" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
expect(subject.commits).to eq(100)
end
end end
describe "#deploys" do describe "#deploys" do
......
...@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do ...@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) } let(:user) { create(:user, namespace: namespace) }
before do before do
create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user) @project_to = fork_project(project_from, user)
end end
......
...@@ -10,7 +10,8 @@ describe API::Helpers, api: true do ...@@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:params) { {} } let(:params) { {} }
let(:env) { {} } let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier) def set_env(token_usr, identifier)
clear_env clear_env
...@@ -52,18 +53,44 @@ describe API::Helpers, api: true do ...@@ -52,18 +53,44 @@ describe API::Helpers, api: true do
describe ".current_user" do describe ".current_user" do
subject { current_user } subject { current_user }
describe "when authenticating via Warden" do describe "Warden authentication" do
before { doorkeeper_guard_returns false } before { doorkeeper_guard_returns false }
context "fails" do context "with invalid credentials" do
context "GET request" do
before { env['REQUEST_METHOD'] = 'GET' }
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end
context "succeeds" do context "with valid credentials" do
before { warden_authenticate_returns user } before { warden_authenticate_returns user }
context "GET request" do
before { env['REQUEST_METHOD'] = 'GET' }
it { is_expected.to eq(user) }
end
context "HEAD request" do
before { env['REQUEST_METHOD'] = 'HEAD' }
it { is_expected.to eq(user) } it { is_expected.to eq(user) }
end end
context "PUT request" do
before { env['REQUEST_METHOD'] = 'PUT' }
it { is_expected.to be_nil }
end
context "POST request" do
before { env['REQUEST_METHOD'] = 'POST' }
it { is_expected.to be_nil }
end
context "DELETE request" do
before { env['REQUEST_METHOD'] = 'DELETE' }
it { is_expected.to be_nil }
end
end
end end
describe "when authenticating using a user's private token" do describe "when authenticating using a user's private token" do
......
...@@ -18,7 +18,7 @@ describe API::API, api: true do ...@@ -18,7 +18,7 @@ describe API::API, api: true do
end end
let(:project_user2) do let(:project_user2) do
create(:project_member, :guest, user: user2, project: project) create(:project_member, :reporter, user: user2, project: project)
end end
describe 'POST /projects/fork/:id' do describe 'POST /projects/fork/:id' do
......
...@@ -111,7 +111,7 @@ describe API::API, api: true do ...@@ -111,7 +111,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username) expect(json_response['username']).to eq(user.username)
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value) expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end end
...@@ -131,7 +131,7 @@ describe API::API, api: true do ...@@ -131,7 +131,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}") expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value) expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end end
end end
......
...@@ -39,7 +39,7 @@ describe JwtController do ...@@ -39,7 +39,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers } subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) } it { expect(response).to have_http_status(401) }
end end
end end
...@@ -77,7 +77,7 @@ describe JwtController do ...@@ -77,7 +77,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers } subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) } it { expect(response).to have_http_status(401) }
end end
end end
......
...@@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do ...@@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file' it_behaves_like 'responds with a file'
end end
describe 'when using a user key' do
let(:authorization) { authorize_user_key }
context 'when user allowed' do
let(:update_permissions) do
project.team << [user, :master]
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'when user not allowed' do
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it 'responds with status 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when build is authorized as' do context 'when build is authorized as' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
...@@ -1131,7 +1154,11 @@ describe 'Git LFS API and storage' do ...@@ -1131,7 +1154,11 @@ describe 'Git LFS API and storage' do
end end
def authorize_deploy_key def authorize_deploy_key
ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate) ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
end
def authorize_user_key
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end end
def fork_project(project, user, object = nil) def fork_project(project, user, object = nil)
......
...@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do ...@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project') description: 'wow such project')
@to_namespace = create(:namespace) @to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace) @to_user = create(:user, namespace: @to_namespace)
@from_project.add_user(@to_user, :developer)
end end
context 'fork project' do context 'fork project' do
context 'when forker is a guest' do
before do
@guest = create(:user)
@from_project.add_user(@guest, :guest)
end
subject { fork_project(@from_project, @guest) }
it { is_expected.not_to be_persisted }
it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
end
describe "successfully creates project in the user namespace" do describe "successfully creates project in the user namespace" do
let(:to_project) { fork_project(@from_project, @to_user) } let(:to_project) { fork_project(@from_project, @to_user) }
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) } it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) } it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero } it { expect(to_project.star_count).to be_zero }
...@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do ...@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
it "fails due to validation, not transaction failure" do it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user) @to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken']) expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken']) expect(@to_project.errors[:path]).to eq(['has already been taken'])
end end
...@@ -81,12 +97,17 @@ describe Projects::ForkService, services: true do ...@@ -81,12 +97,17 @@ describe Projects::ForkService, services: true do
@group = create(:group) @group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER) @group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER) @group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group } @opts = { namespace: @group }
end end
context 'fork project for group' do context 'fork project for group' do
it 'group owner successfully forks project into the group' do it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts) to_project = fork_project(@project, @group_owner, @opts)
expect(to_project).to be_persisted
expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group) expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group) expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name) expect(to_project.name).to eq(@project.name)
......
...@@ -445,7 +445,7 @@ describe SystemNoteService, services: true do ...@@ -445,7 +445,7 @@ describe SystemNoteService, services: true do
end end
context 'commit with cross-reference from fork' do context 'commit with cross-reference from fork' do
let(:author2) { create(:user) } let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) do let(:forked_project) do
fp = Projects::ForkService.new(project, author2).execute fp = Projects::ForkService.new(project, author2).execute
# The call to project.repository.after_import in RepositoryForkWorker does # The call to project.repository.after_import in RepositoryForkWorker does
......
...@@ -4,9 +4,10 @@ module CycleAnalyticsHelpers ...@@ -4,9 +4,10 @@ module CycleAnalyticsHelpers
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end end
def create_commit(message, project, user, branch_name) def create_commit(message, project, user, branch_name, count: 1)
filename = random_git_name
oldrev = project.repository.commit(branch_name).sha oldrev = project.repository.commit(branch_name).sha
commit_shas = Array.new(count) do |index|
filename = random_git_name
options = { options = {
committer: project.repository.user_to_committer(user), committer: project.repository.user_to_committer(user),
...@@ -18,10 +19,13 @@ module CycleAnalyticsHelpers ...@@ -18,10 +19,13 @@ module CycleAnalyticsHelpers
commit_sha = Gitlab::Git::Blob.commit(project.repository, options) commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
project.repository.commit(commit_sha) project.repository.commit(commit_sha)
commit_sha
end
GitPushService.new(project, GitPushService.new(project,
user, user,
oldrev: oldrev, oldrev: oldrev,
newrev: commit_sha, newrev: commit_shas.last,
ref: 'refs/heads/master').execute ref: 'refs/heads/master').execute
end end
......
# These shared examples expect a `snippets` array of snippets
RSpec.shared_examples 'paginated snippets' do |remote: false|
it "is limited to #{Snippet.default_per_page} items per page" do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page)
end
context 'clicking on the link to the second page' do
before do
click_link('2')
wait_for_ajax if remote
end
it 'shows the remaining snippets' do
remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min
expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count)
end
end
end
require 'spec_helper'
describe 'ci/lints/show' do
include Devise::TestHelpers
before do
assign(:status, true)
assign(:stages, %w[test])
assign(:builds, builds)
end
context 'when builds attrbiutes contain HTML nodes' do
let(:builds) do
[ { name: 'rspec', stage: 'test', commands: '<h1>rspec</h1>' } ]
end
it 'does not render HTML elements' do
render
expect(rendered).not_to have_css('h1', text: 'rspec')
end
end
context 'when builds attributes do not contain HTML nodes' do
let(:builds) do
[ { name: 'rspec', stage: 'test', commands: 'rspec' } ]
end
it 'shows configuration in the table' do
render
expect(rendered).to have_css('td pre', text: 'rspec')
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