Commit b8af80cc authored by Ruben Davila's avatar Ruben Davila

Merge remote-tracking branch 'ce/8-12-stable' into 8-12-stable-ee

Conflicts:
	VERSION
	spec/services/system_note_service_spec.rb
parents 576078e8 0816838b
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.12.2 (unreleased) v 8.12.3 (unreleased)
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
......
...@@ -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
......
...@@ -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
...@@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService ...@@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService
end end
end end
def title=(value)
self.properties['title'] = value if self.properties
end
def description def description
if self.properties && self.properties['description'].present? if self.properties && self.properties['description'].present?
self.properties['description'] self.properties['description']
......
...@@ -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:
......
= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
......
- 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)
......
# 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.
...@@ -73,5 +73,7 @@ excluded_attributes: ...@@ -73,5 +73,7 @@ excluded_attributes:
methods: methods:
statuses: statuses:
- :type - :type
services:
- :type
merge_request_diff: merge_request_diff:
- :utf8_st_diffs - :utf8_st_diffs
...@@ -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))
......
...@@ -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",
...@@ -6918,6 +6943,7 @@ ...@@ -6918,6 +6943,7 @@
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"category": "issue_tracker", "category": "issue_tracker",
"type": "CustomIssueTrackerService",
"default": true, "default": true,
"wiki_page_events": true "wiki_page_events": true
}, },
...@@ -7372,5 +7398,16 @@ ...@@ -7372,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,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -107,6 +107,18 @@ 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
restored_project_json
expect(CustomIssueTrackerService.first).not_to be_nil
end
context 'Merge requests' do context 'Merge requests' do
before do before do
restored_project_json restored_project_json
......
...@@ -111,6 +111,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -111,6 +111,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end end
it 'saves the correct service type' do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end
it 'has project feature' do it 'has project feature' do
project_feature = saved_project_json['project_feature'] project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty expect(project_feature).not_to be_empty
...@@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_id: ci_pipeline.sha) commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user) create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
......
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/ }
......
...@@ -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
......
...@@ -25,5 +25,21 @@ describe CustomIssueTrackerService, models: true do ...@@ -25,5 +25,21 @@ describe CustomIssueTrackerService, models: true do
it { is_expected.not_to validate_presence_of(:issues_url) } it { is_expected.not_to validate_presence_of(:issues_url) }
it { is_expected.not_to validate_presence_of(:new_issue_url) } it { is_expected.not_to validate_presence_of(:new_issue_url) }
end end
context 'title' do
let(:issue_tracker) { described_class.new(properties: {}) }
it 'sets a default title' do
issue_tracker.title = nil
expect(issue_tracker.title).to eq('Custom Issue Tracker')
end
it 'sets the custom title' do
issue_tracker.title = 'test title'
expect(issue_tracker.title).to eq('test title')
end
end
end end
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