diff --git a/CHANGELOG b/CHANGELOG
index fcaaa284ce..1005da293a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,24 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.12.2 (unreleased)
+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
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 40c341bdcd..9575d51bad 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.0
+3.6.1
diff --git a/VERSION b/VERSION
index fd32ca6a62..e12be1cc63 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.12.1
+8.12.3
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1cd2302111..7e5e9fa9ae 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/api/:version/projects/:id/labels",
+ labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@@ -65,13 +65,14 @@
return callback(projects);
});
},
- newLabel: function(project_id, data, callback) {
+ newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
- .replace(':id', project_id);
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
- data: data,
+ data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 0decc6d09e..44af1c135a 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -357,7 +357,7 @@
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
// Generate a search result block
- h5 = $('
').text('Search results');
+ h5 = $('').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 1a4d815797..6ccd83e2d8 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
-
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 46d1c3f00c..c5f8c29242 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
- constructor ($el, projectId) {
+ constructor ($el, namespacePath, projectPath) {
this.$el = $el;
- this.projectId = projectId;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
- Api.newLabel(this.projectId, {
- name: this.$newLabelField.val(),
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index be6ebc7794..cdedfd1af1 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,13 +1,9 @@
((w) => {
w.ResolveBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
noteId: Number,
discussionId: String,
resolved: Boolean,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
resolvedBy: String
@@ -69,10 +65,10 @@
if (this.isResolved) {
promise = ResolveService
- .unresolve(this.namespace, this.noteId);
+ .unresolve(this.projectPath, this.noteId);
} else {
promise = ResolveService
- .resolve(this.namespace, this.noteId);
+ .resolve(this.projectPath, this.noteId);
}
promise.then((response) => {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index e373b06b1e..0a61703450 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,12 +1,8 @@
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
discussionId: String,
mergeRequestId: Number,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
},
@@ -50,7 +46,7 @@
},
methods: {
resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
}
},
created: function () {
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
deleted file mode 100644
index d278678085..0000000000
--- a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-((w) => {
- w.ButtonMixins = {
- computed: {
- namespace: function () {
- return `${this.namespacePath}/${this.projectPath}`;
- }
- }
- };
-})(window);
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index de771ff814..2a55f739b3 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
}
- prepareRequest(namespace) {
+ prepareRequest(root) {
this.setCSRF();
- Vue.http.options.root = `/${namespace}`;
+ Vue.http.options.root = root;
}
- resolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ resolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {});
}
- unresolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ unresolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {});
}
- toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved();
let promise;
if (isResolved) {
- promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else {
- promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
}
promise.then((response) => {
@@ -57,10 +57,10 @@
})
}
- resolveAll(namespace, mergeRequestId, discussionId) {
+ resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
@@ -70,10 +70,10 @@
}, {});
}
- unResolveAll(namespace, mergeRequestId, discussionId) {
+ unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 3f15a117ca..2e73e6e10b 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,9 +4,10 @@
var _this;
_this = this;
$('.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);
- projectId = $dropdown.data('project-id');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@@ -24,6 +25,11 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
@@ -35,7 +41,7 @@
$sidebarLabelTooltip.tooltip();
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() {
@@ -43,6 +49,10 @@
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
return this.value;
}).get();
+
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
+
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index f84a20cf0f..b8d52becb3 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -19,7 +19,7 @@
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
- values.push(sParameterName[1]);
+ values.push(sParameterName[1].replace(/\+/g, ' '));
}
i++;
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c6854f703f..866a04d3e2 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -432,14 +432,12 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var namespacePath = $form.attr('data-namespace-path'),
- projectPath = $form.attr('data-project-path')
- discussionId = $form.attr('data-discussion-id'),
- mergeRequestId = $form.attr('data-noteable-iid'),
- namespace = namespacePath + '/' + projectPath;
+ var projectPath = $form.data('project-path')
+ discussionId = $form.data('discussion-id'),
+ mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
}
}
@@ -854,7 +852,6 @@
.closest('form')
.attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true')
- .attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path'));
};
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 34d5d99558..7e4da73bc1 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -25,7 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@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))
end
rescue Gitlab::Auth::MissingPersonalTokenError
@@ -33,10 +33,21 @@ class JwtController < ApplicationController
end
def render_missing_personal_token
- render plain: "HTTP Basic: Access denied\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}",
- status: 401
+ 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 can generate one at #{profile_personal_access_tokens_url}" }
+ ] }, status: 401
+ end
+
+ def render_unauthorized
+ render json: {
+ errors: [
+ { code: 'UNAUTHORIZED',
+ message: 'HTTP Basic: Access denied' }
+ ] }, status: 401
end
def auth_params
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3eb13a121b..ef13e0677d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -54,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- raw_notes = @issue.notes_with_associations.fresh
+ raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 28fa4a5b14..a6626df482 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
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
- render 'new'
+ respond_to do |format|
+ format.html { render 'new' }
+ format.json { render json: { message: @label.errors.messages }, status: 400 }
+ end
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a4bedb3bfe..838ecc837e 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
- html: view_to_html_string("snippets/_snippets", collection: @snippets)
+ html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
}
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 9799f1dc88..29f1c52777 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -109,7 +109,7 @@ class Notify < BaseMailer
headers['X-GitLab-Reply-Key'] = reply_key
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)
end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index 53b2cacb13..b46db449bf 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -10,15 +10,33 @@ class CycleAnalytics
end
def commits
- repository = @project.repository.raw_repository
-
- if @project.default_branch
- repository.log(ref: @project.default_branch, after: @from).count
- end
+ ref = @project.default_branch.presence
+ count_commits_for(ref)
end
def deploys
@project.deployments.where("created_at > ?", @from).count
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
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 63a5ed1448..d9fba3d4a4 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
+ def title=(value)
+ self.properties['title'] = value if self.properties
+ end
+
def description
if self.properties && self.properties['description'].present?
self.properties['description']
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 38ac663122..8ea88da8a5 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -7,10 +7,10 @@ module Auth
def execute(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
- return error('forbidden', 403) unless scope
+ return error('DENIED', status: 403, message: 'access forbidden') unless scope
end
{ token: authorized_token(scope).encoded }
@@ -111,5 +111,12 @@ module Auth
@authentication_abilities.include?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
+
+ def error(code, status:, message: '')
+ {
+ errors: [{ code: code, message: message }],
+ http_status: status
+ }
+ end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index be749ba4a1..696fe3efe8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -15,6 +15,11 @@ module Projects
return @project
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
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@@ -71,6 +76,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
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)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index a2de4dccec..a2b23ea617 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -16,6 +16,8 @@ module Projects
end
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
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f7875e68b7..1545c00af4 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -16,8 +16,7 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
- %pre
- = simple_format build[:commands]
+ %pre= build[:commands]
%br
%b Tag list:
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 689cd6ed66..2ef383960f 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,4 +1,4 @@
-= 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"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable?
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index 7a8767ddba..f0b61e0f7d 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,11 +1,10 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
- ":project-path" => "'#{discussion.project.path}'",
+ %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
.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")
{{ buttonText }}
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index f41a11a056..95a8c7b4a5 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -8,13 +8,7 @@
%tbody
%th Status
%th Commit
- - pipelines.stages.each do |stage|
- %th.stage
- - if stage.titleize.length > 12
- %span.has-tooltip{ title: "#{stage.titleize}" }
- = stage.titleize
- - else
- = stage.titleize
+ %th Stages
%th
%th
= render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 3900b4f6f1..cfb44bd206 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -5,7 +5,7 @@
- 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"}
%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 }}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 4981951975..904452fcc4 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -58,11 +58,11 @@
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 9ec17cf6e7..788be4a004 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -24,14 +24,12 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
-
- %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
- ":project-path" => "'#{note.project.path}'",
- ":discussion-id" => "'#{note.discussion_id}'",
+ %resolve-btn{ "project-path" => "#{project_path(note.project)}",
+ "discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
- ":resolved-by" => "'#{note.resolved_by.try(:name)}'",
+ "resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"v-ref:note_#{note.id}" => true }
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index cf26197f7d..f17096968f 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -37,7 +37,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.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
.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" }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d34d28f673..ebe441c931 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -4,7 +4,7 @@
- show_footer = local_assigns.fetch(:show_footer, true)
- data_options = local_assigns.fetch(:data_options, {})
- 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)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b13daaf43c..0464ebddc1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -128,7 +128,7 @@
- issuable.labels_array.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.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
Label
= icon('chevron-down')
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 7be4a47157..77b66ca74b 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,3 +1,5 @@
+- remote = local_assigns.fetch(:remote, false)
+
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
@@ -5,7 +7,7 @@
%li
.nothing-here-block Nothing here.
- = paginate @snippets, theme: 'gitlab', remote: true
+ = paginate @snippets, theme: 'gitlab', remote: remote
:javascript
gl.SnippetsList();
diff --git a/config/application.rb b/config/application.rb
index 4792f6670a..1ebdb43d66 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -99,13 +99,24 @@ module Gitlab
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
- 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
origins '*'
resource '/api/*',
+ credentials: false,
headers: :any,
methods: :any,
expose: ['Link']
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 8abeb5ee24..70dbd03000 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
+ @project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 714d4ea3dc..8b8c4eb4d4 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -21,8 +21,11 @@ module API
end
# 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
- warden ? warden.authenticate : nil
+ warden.try(:authenticate) if request.get? || request.head?
end
def find_user_by_private_token
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 090d04544d..9a5d1ece07 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -90,7 +90,7 @@ module API
{
username: token_handler.actor_name,
- lfs_token: token_handler.generate,
+ lfs_token: token_handler.token,
repository_http_path: project.http_url_to_repo
}
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 7c0f2115d4..aca5d0020c 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -124,7 +124,7 @@ module Gitlab
read_authentication_abilities
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
def build_access_token_check(login, password)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 88803d7662..bb9d108033 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,5 +1,8 @@
# Model relationships to be included in the project import/export
project_tree:
+ - :labels
+ - milestones:
+ - :events
- issues:
- :events
- notes:
@@ -39,9 +42,6 @@ project_tree:
- protected_branches:
- :merge_access_levels
- :push_access_levels
- - :labels
- - milestones:
- - :events
- :project_feature
# Only include the following attributes for the models specified.
@@ -73,5 +73,7 @@ excluded_attributes:
methods:
statuses:
- :type
+ services:
+ - :type
merge_request_diff:
- :utf8_st_diffs
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c7b3551b84..35ff134ea1 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -61,11 +61,17 @@ module Gitlab
def restore_project
return @project unless @tree_hash
- project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
@project.update(project_params)
@project
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,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index d089a2f9b0..7b3bbcf6a3 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -17,19 +17,18 @@ module Gitlab
end
end
- def generate
- token = Devise.friendly_token(TOKEN_LENGTH)
-
+ def token
Gitlab::Redis.with do |redis|
- redis.set(redis_key, token, ex: EXPIRY_TIME)
- end
+ token = redis.get(redis_key)
- token
- end
+ if token
+ redis.expire(redis_key, EXPIRY_TIME)
+ else
+ token = Devise.friendly_token(TOKEN_LENGTH)
+ redis.set(redis_key, token, ex: EXPIRY_TIME)
+ end
- def value
- Gitlab::Redis.with do |redis|
- redis.get(redis_key)
+ token
end
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 54a2d3d946..19a8b1fe52 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,8 +73,8 @@ describe UsersController do
end
context 'forked project' do
- let!(:project) { create(:project) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:project) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 19941978c5..1fd276d5b1 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -62,6 +62,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') }
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!(:list2) { create(:list, board: project.board, label: development, position: 1) }
@@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
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
visit namespace_project_board_path(project.namespace, project)
@@ -441,6 +442,34 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_empty_boards((2..4))
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
50.times do
create(:labeled_issue, project: project, labels: [testing])
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
new file mode 100644
index 0000000000..62937688c2
--- /dev/null
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -0,0 +1,15 @@
+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
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 22359c8f93..9fe40ea089 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -369,6 +369,24 @@ describe 'Issues', feature: true do
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
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
new file mode 100644
index 0000000000..d37e8ed469
--- /dev/null
+++ b/spec/features/projects/snippets_spec.rb
@@ -0,0 +1,14 @@
+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
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
new file mode 100644
index 0000000000..70b16bfc81
--- /dev/null
+++ b/spec/features/snippets_spec.rb
@@ -0,0 +1,14 @@
+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
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index cc40671787..33b52d1547 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do
let(:mail) { ActionMailer::Base.deliveries.last }
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'] }
before do
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index f00abd82fe..ce7e809ec7 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -3,30 +3,16 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user) }
-
context 'when the user has snippets' do
+ let(:user) { create(:user) }
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
before do
- create_list(:snippet, 25, :public, author: user)
-
+ allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax
end
- it 'is limited to 20 items per page' do
- 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
+ it_behaves_like 'paginated snippets', remote: true
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 70032e7df9..bcd53440cb 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
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 }
it "returns false if there are no appropriate permissions" do
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 840c7b6d01..1ad6f61221 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -48,9 +48,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -70,9 +70,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -86,4 +86,3 @@
});
});
})();
-
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 745fbc0df4..c9d64e99f8 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes user lfs tokens' do
user = create(:user)
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.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
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
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.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))
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 281f6cf117..98323fe6be 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2231,6 +2231,31 @@
],
"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,
"title": "v4.0",
@@ -6918,6 +6943,7 @@
"note_events": true,
"build_events": true,
"category": "issue_tracker",
+ "type": "CustomIssueTrackerService",
"default": true,
"wiki_page_events": true
},
@@ -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
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index feacb29523..7582a732cd 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -107,6 +107,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
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
before do
restored_project_json
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d891c2d0cc..cf8f2200c5 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -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
end
+ it 'saves the correct service type' do
+ expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
+ end
+
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
@@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_id: ci_pipeline.sha)
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(:wiki_access_level, ProjectFeature::ENABLED)
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 9f04f67e0a..e9c1163e22 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::LfsToken, lib: true do
- describe '#generate and #value' do
+ describe '#token' do
shared_examples 'an LFS token generator' do
it 'returns a randomly generated token' do
- token = handler.generate
+ token = handler.token
expect(token).not_to be_nil
expect(token).to be_a String
@@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do
end
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
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 56872da9a8..5c9851f14c 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -169,8 +169,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
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', /^<.+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
index 743bc2da33..9d67bc82cb 100644
--- a/spec/models/cycle_analytics/summary_spec.rb
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do
expect(subject.commits).to eq(0)
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
describe "#deploys" do
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 9c81d159cd..1863581f57 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) }
before do
+ create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user)
end
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 7020667ea5..63320931e7 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -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(:new_issue_url) }
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
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index e66faeed70..0f41f8dc7f 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) }
let(:params) { {} }
- let(:env) { {} }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier)
clear_env
@@ -52,17 +53,43 @@ describe API::Helpers, api: true do
describe ".current_user" do
subject { current_user }
- describe "when authenticating via Warden" do
+ describe "Warden authentication" do
before { doorkeeper_guard_returns false }
- context "fails" do
- it { is_expected.to be_nil }
+ context "with invalid credentials" do
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to be_nil }
+ end
end
- context "succeeds" do
+ context "with valid credentials" do
before { warden_authenticate_returns user }
- it { is_expected.to eq(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) }
+ 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
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 34f84f7895..e38d5745d4 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
end
let(:project_user2) do
- create(:project_member, :guest, user: user2, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 46e8e6f116..f0f590b033 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -111,7 +111,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
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)
end
@@ -131,7 +131,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
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)
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 6b956e6300..f0ef155bd7 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -39,7 +39,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
@@ -77,7 +77,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 09e4e265dd..dbdf83a0df 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
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
let(:authorization) { authorize_ci_project }
@@ -1110,7 +1133,11 @@ describe 'Git LFS API and storage' do
end
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
def fork_project(project, user, object = nil)
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ef2036c78b..64d15c0523 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
+ @from_project.add_user(@to_user, :developer)
end
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
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.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true 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)
@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[:path]).to eq(['has already been taken'])
end
@@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
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.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ expect(to_project.star_count).to be_zero
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3d854a959f..a3f16e2d94 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -445,7 +445,7 @@ describe SystemNoteService, services: true do
end
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) { Projects::ForkService.new(project, author2).execute }
let(:commit2) { forked_project.commit }
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index e8e760a618..62a5b46d47 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -4,24 +4,28 @@ module CycleAnalyticsHelpers
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
- def create_commit(message, project, user, branch_name)
- filename = random_git_name
+ def create_commit(message, project, user, branch_name, count: 1)
oldrev = project.repository.commit(branch_name).sha
+ commit_shas = Array.new(count) do |index|
+ filename = random_git_name
- options = {
- committer: project.repository.user_to_committer(user),
- author: project.repository.user_to_committer(user),
- commit: { message: message, branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
+ options = {
+ committer: project.repository.user_to_committer(user),
+ author: project.repository.user_to_committer(user),
+ commit: { message: message, branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
- commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
- project.repository.commit(commit_sha)
+ commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ project.repository.commit(commit_sha)
+
+ commit_sha
+ end
GitPushService.new(project,
user,
oldrev: oldrev,
- newrev: commit_sha,
+ newrev: commit_shas.last,
ref: 'refs/heads/master').execute
end
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb
new file mode 100644
index 0000000000..57dfff3471
--- /dev/null
+++ b/spec/support/snippets_shared_examples.rb
@@ -0,0 +1,18 @@
+# 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
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
new file mode 100644
index 0000000000..3a65a86cd8
--- /dev/null
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -0,0 +1,35 @@
+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: 'rspec
' } ]
+ 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