New upstream version 8.12.3+dfsg1
This commit is contained in:
parent
ab0093fc8a
commit
4d449cfe16
72 changed files with 554 additions and 186 deletions
20
CHANGELOG
20
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
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.6.0
|
||||
3.6.1
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
8.12.1
|
||||
8.12.3
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -357,7 +357,7 @@
|
|||
$('ul.emoji-menu-search, h5.emoji-search').remove();
|
||||
if (term) {
|
||||
// Generate a search result block
|
||||
h5 = $('<h5>').text('Search results');
|
||||
h5 = $('<h5 class="emoji-search" />').text('Search results');
|
||||
found_emojis = _this.searchEmojis(term).show();
|
||||
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
((w) => {
|
||||
w.ButtonMixins = {
|
||||
computed: {
|
||||
namespace: function () {
|
||||
return `${this.namespacePath}/${this.projectPath}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
})(window);
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
%tr
|
||||
%td #{stage.capitalize} Job - #{build[:name]}
|
||||
%td
|
||||
%pre
|
||||
= simple_format build[:commands]
|
||||
%pre= build[:commands]
|
||||
|
||||
%br
|
||||
%b Tag list:
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
15
spec/features/dashboard/snippets_spec.rb
Normal file
15
spec/features/dashboard/snippets_spec.rb
Normal file
|
@ -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
|
|
@ -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) }
|
||||
|
||||
|
|
14
spec/features/projects/snippets_spec.rb
Normal file
14
spec/features/projects/snippets_spec.rb
Normal file
|
@ -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
|
14
spec/features/snippets_spec.rb
Normal file
14
spec/features/snippets_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@
|
|||
});
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/ }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
18
spec/support/snippets_shared_examples.rb
Normal file
18
spec/support/snippets_shared_examples.rb
Normal file
|
@ -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
|
35
spec/views/ci/lints/show.html.haml_spec.rb
Normal file
35
spec/views/ci/lints/show.html.haml_spec.rb
Normal file
|
@ -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: '<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
|
Loading…
Reference in a new issue