Commit f435bc96 authored by Makoto Scott-Hinkle's avatar Makoto Scott-Hinkle

Merge branch 'master' into 22452-milestone-title-unnecessary-escaping-fix

parents f837238a 3b206ccb
Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Speed-up group milestones show page
- Add more tests for calendar contribution (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Only update issuable labels if they have been changed
- Revoke button in Applications Settings underlines on hover.
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
- Optimize GitHub importing for speed and memory
v 8.12.2 (unreleased)
- 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
v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
......@@ -115,6 +125,7 @@ v 8.12.0
- Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
- Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps)
- Fix issue board label filtering appending already filtered labels
- Add search to all issue board lists
- Scroll active tab into view on mobile
- Fix groups sort dropdown alignment (ClemMakesApps)
......
......@@ -7,7 +7,7 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
gem 'sprockets-es6'
gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
......@@ -120,8 +120,8 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
gem 'unicorn', '~> 4.9.0'
gem 'unicorn-worker-killer', '~> 0.4.2'
gem 'unicorn', '~> 5.1.0'
gem 'unicorn-worker-killer', '~> 0.4.4'
end
# State machine
......@@ -210,7 +210,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.0'
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
......
......@@ -553,7 +553,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
raindrops (0.15.0)
raindrops (0.17.0)
rake (10.5.0)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
......@@ -644,7 +644,7 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
sass-rails (5.0.5)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
......@@ -708,7 +708,7 @@ GEM
sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-es6 (0.9.0)
sprockets-es6 (0.9.2)
babel-source (>= 5.8.11)
babel-transpiler
sprockets (>= 3.0.0)
......@@ -759,9 +759,8 @@ GEM
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.1)
unicorn (4.9.0)
unicorn (5.1.0)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
......@@ -945,7 +944,7 @@ DEPENDENCIES
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.15.9)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
......@@ -965,7 +964,7 @@ DEPENDENCIES
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.7.0)
sprockets-es6
sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
......@@ -979,8 +978,8 @@ DEPENDENCIES
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 4.9.0)
unicorn-worker-killer (~> 0.4.2)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.2)
......
......@@ -4,7 +4,7 @@
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, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
......@@ -24,6 +24,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('/');
}
......@@ -43,6 +48,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;
......@@ -280,12 +289,12 @@
if (page === 'projects:boards:show') {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
} else if (label.title) {
} else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
} else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
filters = filters.filter(function (label) {
return label !== $el.text().trim();
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
});
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
}
......
......@@ -389,4 +389,41 @@
})();
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path')
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path')
};
}
});
}).call(this);
......@@ -103,7 +103,7 @@
// Custom dropdown positioning
.dropdown-menu {
top: 30px;
top: 37px;
left: -5px;
padding: 0;
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
......@@ -10,11 +9,7 @@ module MembershipActions
end
def approve_access_request
@member = membershipable.requesters.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
redirect_to polymorphic_url([membershipable, :members])
end
......
......@@ -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" \
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
"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
......
......@@ -73,7 +73,8 @@ class ProfilesController < Profiles::ApplicationController
:skype,
:twitter,
:username,
:website_url
:website_url,
:organization
)
end
end
......@@ -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)
......
module LfsHelper
include Gitlab::Routing.url_helpers
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
status: 501
)
......@@ -46,7 +48,7 @@ module LfsHelper
render(
json: {
message: 'Access forbidden. Check your access level.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 403
......@@ -57,7 +59,7 @@ module LfsHelper
render(
json: {
message: 'Not found.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 404
......
......@@ -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
......
......@@ -4,4 +4,12 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
validates :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def done_list
lists.merge(List.done).take
end
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
......@@ -590,13 +590,11 @@ class MergeRequest < ActiveRecord::Base
end
def merge_commit_message
message = "Merge branch '#{source_branch}' into '#{target_branch}'"
message << "\n\n"
message << title.to_s
message << "\n\n"
message << description.to_s
message << "\n\n"
message << "See merge request !#{iid}"
message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
message << "#{title}\n\n"
message << "#{description}\n\n" if description.present?
message << "See merge request #{to_reference}"
message
end
......
......@@ -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
module Members
class ApproveAccessRequestService < BaseService
include MembersHelper
attr_accessor :source
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
access_requester = source.requesters.find_by!(condition)
raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
access_requester
end
private
def can_update_access_requester?(access_requester)
access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
end
end
end
......@@ -5,6 +5,6 @@
":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", "v-cloak": true }
%button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
= icon("spinner spin", "v-show" => "loading")
{{ buttonText }}
......@@ -2,15 +2,18 @@
- label = 'This group'
- if controller.controller_path =~ /^projects/ && @project.persisted?
- label = 'This project'
- if @group && @group.persisted? && @group.path
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- if label.present?
.location-badge= label
.search-input-wrap
.dropdown{ data: {url: search_autocomplete_path } }
= search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
.dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
......@@ -21,8 +24,9 @@
%i.search-icon
%i.clear-icon.js-clear-input
= hidden_field_tag :group_id, @group.try(:id)
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
= hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs
- if @project && @project.persisted?
- if current_controller?(:issues)
......@@ -36,31 +40,6 @@
- else
= hidden_field_tag :search_code, true
:javascript
gl.projectOptions = gl.projectOptions || {};
gl.projectOptions["#{j(@project.path)}"] = {
issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
name: "#{j(@project.name)}"
};
- if @group && @group.persisted? && @group.path
:javascript
gl.groupOptions = gl.groupOptions || {};
gl.groupOptions["#{j(@group.path)}"] = {
name: "#{j(@group.name)}",
issuesPath: "#{issues_group_path(j(@group.path))}",
mrPath: "#{merge_requests_group_path(j(@group.path))}"
};
:javascript
gl.dashboardOptions = {
issuesPath: "#{issues_dashboard_url}",
mrPath: "#{merge_requests_dashboard_url}"
};
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
......
......@@ -86,6 +86,9 @@
.form-group
= f.label :location, 'Location', class: "label-light"
= f.text_field :location, class: "form-control"
.form-group
= f.label :organization, 'Organization', class: "label-light"
= f.text_field :organization, class: "form-control"
.form-group
= f.label :bio, class: "label-light"
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
......
......@@ -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
......@@ -70,6 +70,10 @@
.profile-link-holder.middle-dot-divider
= icon('map-marker')
= @user.location
- unless @user.organization.blank?
.profile-link-holder.middle-dot-divider
= icon('building')
= @user.organization
- if @user.bio.present?
.cover-desc
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddOrganizationToUser < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :users, :organization, :string
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160920160832) do
ActiveRecord::Schema.define(version: 20160926145521) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1132,6 +1132,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
t.string "organization"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
......@@ -57,6 +57,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
......@@ -89,6 +90,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
"theme_id": 1,
......@@ -147,7 +149,8 @@ Parameters:
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": ""
"website_url": "",
"organization": ""
}
```
......@@ -178,6 +181,7 @@ Parameters:
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
......@@ -214,6 +218,7 @@ Parameters:
- `linkedin` (optional) - LinkedIn
- `twitter` (optional) - Twitter account
- `website_url` (optional) - Website URL
- `organization` (optional) - Organization name
- `projects_limit` (optional) - Number of projects user can create
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
......@@ -242,6 +247,7 @@ Parameters:
- `linkedin` - LinkedIn
- `twitter` - Twitter account
- `website_url` - Website URL
- `organization` - Organization name
- `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID
- `provider` - External provider name
......@@ -296,6 +302,7 @@ GET /user
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
......
......@@ -13,6 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
@user.reload
end
......@@ -23,6 +24,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
expect(@user.twitter).to eq 'testtwitter'
expect(@user.website_url).to eq 'testurl'
expect(@user.bio).to eq 'I <3 GitLab'
expect(@user.organization).to eq 'GitLab'
expect(find('#user_location').value).to eq 'Ukraine'
end
......
......@@ -55,13 +55,8 @@ module API
put ':id/access_requests/:user_id/approve' do
required_attributes! [:user_id]
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
member = source.requesters.find_by!(user_id: params[:user_id])
if params[:access_level]
member.update(access_level: params[:access_level])
end
member.accept_request
member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute
status :created
present member.user, with: Entities::Member, member: member
......
......@@ -15,7 +15,7 @@ module API
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
expose :bio, :location, :skype, :linkedin, :twitter, :website_url
expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
end
class Identity < Grape::Entity
......
......@@ -60,6 +60,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
# organization - Organization
# projects_limit - Number of projects user can create
# extern_uid - External authentication provider UID
# provider - External provider
......@@ -74,7 +75,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
user = User.build_user(attrs)
......@@ -111,6 +112,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
# organization - Organization
# projects_limit - Limit projects each user can create
# bio - Bio
# location - Location of the user
......@@ -122,7 +124,7 @@ module API
put ":id" do
authenticated_as_admin!
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
user = User.find(params[:id])
not_found!('User') unless user
......
......@@ -52,7 +52,7 @@ module Gitlab
def method_missing(method, *args, &block)
if api.respond_to?(method)
request { api.send(method, *args, &block) }
request(method, *args, &block)
else
super(method, *args, &block)
end
......@@ -99,20 +99,19 @@ module Gitlab
rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
end
def request
def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
data = yield
data = api.send(method, *args, &block)
yield data
last_response = api.last_response
while last_response.rels[:next]
sleep rate_limit_sleep_time if rate_limit_exceed?
last_response = last_response.rels[:next].get
data.concat(last_response.data) if last_response.data.is_a?(Array)
yield last_response.data if last_response.data.is_a?(Array)
end
data
end
end
end
......
......@@ -10,6 +10,7 @@ module Gitlab
@repo = project.import_source
@repo_url = project.import_url
@errors = []
@labels = {}
if credentials
@client = Client.new(credentials[:user])
......@@ -23,6 +24,7 @@ module Gitlab
import_milestones
import_issues
import_pull_requests
import_comments
import_wiki
import_releases
handle_errors
......@@ -46,20 +48,20 @@ module Gitlab
end
def import_labels
labels = client.labels(repo, per_page: 100)
client.labels(repo, per_page: 100) do |labels|
labels.each do |raw|
begin
LabelFormatter.new(project, raw).create!
label = LabelFormatter.new(project, raw).create!
@labels[label.title] = label.id
rescue => e
errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end
end
end
def import_milestones
milestones = client.milestones(repo, state: :all, per_page: 100)
client.milestones(repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
begin
MilestoneFormatter.new(project, raw).create!
......@@ -68,38 +70,37 @@ module Gitlab
end
end
end
end
def import_issues
issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw)
if gh_issue.valid?
begin
issue = gh_issue.create!
apply_labels(issue)
import_comments(issue) if gh_issue.has_comments?
apply_labels(issue, raw)
rescue => e
errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end
end
end
end
def import_pull_requests
pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
pull_request = PullRequestFormatter.new(project, raw)
next unless pull_request.valid?
pull_requests.each do |pull_request|
begin
restore_source_branch(pull_request) unless pull_request.source_branch_exists?
restore_target_branch(pull_request) unless pull_request.target_branch_exists?
merge_request = pull_request.create!
apply_labels(merge_request)
import_comments(merge_request)
import_comments_on_diff(merge_request)
apply_labels(merge_request, raw)
rescue => e
errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
ensure
......@@ -108,6 +109,9 @@ module Gitlab
end
end
project.repository.after_remove_branch
end
def restore_source_branch(pull_request)
project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name)
end
......@@ -125,37 +129,38 @@ module Gitlab
def clean_up_restored_branches(pull_request)
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
project.repository.after_remove_branch
end
def apply_labels(issuable)
issue = client.issue(repo, issuable.iid)
if issue.labels.count > 0
label_ids = issue.labels
.map { |attrs| project.labels.find_by(title: attrs.name).try(:id) }
def apply_labels(issuable, raw_issuable)
if raw_issuable.labels.count > 0
label_ids = raw_issuable.labels
.map { |attrs| @labels[attrs.name] }
.compact
issuable.update_attribute(:label_ids, label_ids)
end
end
def import_comments(issuable)
comments = client.issue_comments(repo, issuable.iid, per_page: 100)
create_comments(issuable, comments)
def import_comments
client.issues_comments(repo, per_page: 100) do |comments|
create_comments(comments, :issue)
end
def import_comments_on_diff(merge_request)
comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100)
create_comments(merge_request, comments)
client.pull_requests_comments(repo, per_page: 100) do |comments|
create_comments(comments, :pull_request)
end
end
def create_comments(issuable, comments)
def create_comments(comments, issuable_type)
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
comment = CommentFormatter.new(project, raw)
issuable_class = issuable_type == :issue ? Issue : MergeRequest
iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly
issuable = issuable_class.find_by_iid(iid)
next unless issuable
issuable.notes.create!(comment.attributes)
rescue => e
errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
......@@ -180,7 +185,7 @@ module Gitlab
end
def import_releases
releases = client.releases(repo, per_page: 100)
client.releases(repo, per_page: 100) do |releases|
releases.each do |raw|
begin
gh_release = ReleaseFormatter.new(project, raw)
......@@ -192,4 +197,5 @@ module Gitlab
end
end
end
end
end
# 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.
......
......@@ -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+
......
......@@ -2,9 +2,10 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
describe '#index' do
let(:group) { create(:group) }
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
......
......@@ -20,10 +20,7 @@ describe Projects::Boards::ListsController do
end
it 'returns a list of board lists' do
board = project.create_board
create(:backlog_list, board: board)
create(:list, board: board)
create(:done_list, board: board)
read_board_list user: user
......
......@@ -106,6 +106,8 @@ FactoryGirl.define do
factory :project_with_board, parent: :empty_project do
after(:create) do |project|
project.create_board
project.board.lists.create(list_type: :backlog)
project.board.lists.create(list_type: :done)
end
end
end
......@@ -4,15 +4,11 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:project) { create(:project_with_board, :public) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
before do
project.create_board
project.board.lists.create(list_type: :backlog)
project.board.lists.create(list_type: :done)
project.team << [user, :master]
project.team << [user2, :master]
......@@ -470,6 +466,29 @@ describe 'Issue Boards', feature: true, js: true do
end
end
it 'removes filtered labels' do
wait_for_vue_resource
page.within '.labels-filter' do
click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link(testing.title)
wait_for_vue_resource(spinner: false)
end
expect(page).to have_css('input[name="label_name[]"]', visible: false)
page.within '.dropdown-menu-labels' do
click_link(testing.title)
wait_for_vue_resource(spinner: false)
end
expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
end
end
it 'infinite scrolls list with label filter' do
50.times do
create(:labeled_issue, project: project, labels: [testing])
......
......@@ -5,13 +5,41 @@ feature 'Contributions Calendar', js: true, feature: true do
let(:contributed_project) { create(:project, :public) }
before do
login_as :user
date_format = '%A %b %d, %Y'
issue_title = 'Bug in old browser'
issue_params = { title: issue_title }
issue_params = { title: 'Bug in old browser' }
Issues::CreateService.new(contributed_project, @user, issue_params).execute
def get_cell_color_selector(contributions)
contribution_cell = '.user-contrib-cell'
activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77']
activity_colors_index = 0
if contributions > 0 && contributions < 10
activity_colors_index = 1
elsif contributions >= 10 && contributions < 20
activity_colors_index = 2
elsif contributions >= 20 && contributions < 30
activity_colors_index = 3
elsif contributions >= 30
activity_colors_index = 4
end
"#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']"
end
# Push code contribution
def get_cell_date_selector(contributions, date)
contribution_text = 'No contributions'
if contributions === 1
contribution_text = '1 contribution'
elsif contributions > 1
contribution_text = "#{contributions} contributions"
end
"#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']"
end
def push_code_contribution
push_params = {
project: contributed_project,
action: Event::PUSHED,
......@@ -20,7 +48,10 @@ feature 'Contributions Calendar', js: true, feature: true do
}
Event.create(push_params)
end
before do
login_as :user
visit @user.username
wait_for_ajax
end
......@@ -29,11 +60,71 @@ feature 'Contributions Calendar', js: true, feature: true do
expect(page).to have_css('.js-contrib-calendar')
end
describe '1 calendar activity' do
before do
Issues::CreateService.new(contributed_project, @user, issue_params).execute
visit @user.username
wait_for_ajax
end
it 'displays calendar activity log', js: true do
expect(find('.content_list .event-note')).to have_content "Bug in old browser"
expect(find('.content_list .event-note')).to have_content issue_title
end
it 'displays calendar activity square color for 1 contribution', js: true do
expect(page).to have_selector(get_cell_color_selector(1), count: 1)
end
it 'displays calendar activity square on the correct date', js: true do
today = Date.today.strftime(date_format)
expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
end
end
it 'displays calendar activity square color', js: true do
expect(page).to have_selector('.user-contrib-cell[fill=\'#acd5f2\']', count: 1)
describe '10 calendar activities' do
before do
(0..9).each do |i|
push_code_contribution()
end
visit @user.username
wait_for_ajax
end
it 'displays calendar activity square color for 10 contributions', js: true do
expect(page).to have_selector(get_cell_color_selector(10), count: 1)
end
it 'displays calendar activity square on the correct date', js: true do
today = Date.today.strftime(date_format)
expect(page).to have_selector(get_cell_date_selector(10, today), count: 1)
end
end
describe 'calendar activity on two days' do
before do
push_code_contribution()
Timecop.freeze(Date.yesterday)
Issues::CreateService.new(contributed_project, @user, issue_params).execute
Timecop.return
visit @user.username
wait_for_ajax
end
it 'displays calendar activity squares for both days', js: true do
expect(page).to have_selector(get_cell_color_selector(1), count: 2)
end
it 'displays calendar activity square for yesterday', js: true do
yesterday = Date.yesterday.strftime(date_format)
expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
end
it 'displays calendar activity square for today', js: true do
today = Date.today.strftime(date_format)
expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
end
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) }
......
......@@ -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
......
......@@ -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 @@
});
});
})();
......@@ -66,6 +66,6 @@ describe Gitlab::GithubImport::Client, lib: true do
stub_request(:get, /api.github.com/)
allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
expect { client.issues }.not_to raise_error
expect { client.issues {} }.not_to raise_error
end
end
......@@ -57,7 +57,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
labels: [double(name: 'Label #1')],
)
end
......@@ -75,7 +76,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
labels: [double(name: 'Label #2')],
)
end
......@@ -94,7 +96,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
labels: [double(name: 'Label #3')],
)
end
......@@ -129,6 +132,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
......@@ -148,9 +153,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
errors: [
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
{ type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
......
......@@ -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",
......@@ -7373,5 +7398,16 @@
}
]
}
]
],
"project_feature": {
"builds_access_level": 0,
"created_at": "2014-12-26T09:26:45.000Z",
"id": 2,
"issues_access_level": 0,
"merge_requests_access_level": 20,
"project_id": 4,
"snippets_access_level": 20,
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
}
\ No newline at end of file
......@@ -107,6 +107,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
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
......
......@@ -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
......
......@@ -328,6 +328,42 @@ describe MergeRequest, models: true do
end
end
describe '#merge_commit_message' do
it 'includes merge information as the title' do
request = build(:merge_request, source_branch: 'source', target_branch: 'target')
expect(request.merge_commit_message)
.to match("Merge branch 'source' into 'target'\n\n")
end
it 'includes its title in the body' do
request = build(:merge_request, title: 'Remove all technical debt')
expect(request.merge_commit_message)
.to match("Remove all technical debt\n\n")
end
it 'includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
expect(request.merge_commit_message)
.to match("By removing all code\n\n")
end
it 'includes its reference in the body' do
request = build_stubbed(:merge_request)
expect(request.merge_commit_message)
.to match("See merge request #{request.to_reference}")
end
it 'excludes multiple linebreak runs when description is blank' do
request = build(:merge_request, title: 'Title', description: nil)
expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
end
end
describe "#reset_merge_when_build_succeeds" do
let(:merge_if_green) do
create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
......
......@@ -62,6 +62,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
expect(json_response.first.keys).to include 'organization'
expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled'
......@@ -265,6 +266,14 @@ describe API::API, api: true do
expect(user.reload.bio).to eq('new test bio')
end
it "updates user with organization" do
put api("/users/#{user.id}", admin), { organization: 'GitLab' }
expect(response).to have_http_status(200)
expect(json_response['organization']).to eq('GitLab')
expect(user.reload.organization).to eq('GitLab')
end
it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
expect(response).to have_http_status(200)
......
......@@ -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
......
......@@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
let!(:done) { project.board.done_list }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
......
......@@ -10,10 +10,10 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
let!(:done) { project.board.done_list }
before do
project.team << [user, :developer]
......
......@@ -17,17 +17,15 @@ describe Boards::Lists::CreateService, services: true do
end
end
context 'when board lists has only a backlog list' do
context 'when board lists has backlog, and done lists' do
it 'creates a new list at beginning of the list' do
create(:backlog_list, board: board)
list = service.execute
expect(list.position).to eq 0
end
end
context 'when board lists has only labels lists' do
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
......@@ -40,8 +38,6 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists has backlog, label and done lists' do
it 'creates a new list at end of the label lists' do
create(:backlog_list, board: board)
create(:done_list, board: board)
list1 = create(:list, board: board, position: 0)
list2 = service.execute
......
......@@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
backlog = create(:backlog_list, board: board)
backlog = project.board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
done = create(:done_list, board: board)
done = project.board.done_list
described_class.new(project, user).execute(development)
......@@ -31,14 +31,14 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'does not remove list from board when list type is backlog' do
list = create(:backlog_list, board: board)
list = project.board.backlog_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
it 'does not remove list from board when list type is done' do
list = create(:done_list, board: board)
list = project.board.done_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
......
......@@ -6,12 +6,12 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { project.board }
let(:user) { create(:user) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:backlog) { project.board.backlog_list }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
let!(:done) { create(:done_list, board: board) }
let!(:done) { project.board.done_list }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
......
require 'spec_helper'
describe Members::ApproveAccessRequestService, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
member = described_class.new(source, user, params).execute
expect(member).to be_a "#{source.class.to_s}Member".constantize
expect(member.requested_at).to be_nil
end
context 'with a custom access level' do
let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } }
it 'returns a ProjectMember with the custom access level' do
member = described_class.new(source, user, params).execute
expect(member.access_level).to eq Gitlab::Access::MASTER
end
end
end
context 'when no access requester are found' do
let(:params) { { user_id: 42 } }
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { project }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
end
end
context 'when an access requester is found' do
before do
project.request_access(access_requester)
group.request_access(access_requester)
end
let(:params) { { user_id: access_requester.id } }
context 'when current user cannot approve access request to the project' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end
context 'when current user can approve access request to the project' do
before do
project.team << [user, :master]
group.add_owner(user)
end
it_behaves_like 'a service approving an access request' do
let(:source) { project }
end
it_behaves_like 'a service approving an access request' do
let(:source) { group }
end
context 'when given a :id' do
let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
it_behaves_like 'a service approving an access request' do
let(:source) { project }
end
end
end
end
end
......@@ -4,9 +4,10 @@ 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),
......@@ -18,10 +19,13 @@ module CycleAnalyticsHelpers
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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment