summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md12
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 (renamed from app/assets/javascripts/cycle_analytics.js.es6)0
-rw-r--r--app/assets/javascripts/gl_dropdown.js11
-rw-r--r--app/assets/javascripts/issue.js6
-rw-r--r--app/assets/javascripts/merge_request.js6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss12
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/helpers/issuables_helper.rb8
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/taskable.rb16
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/lfs_object.rb6
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/repository.rb14
-rw-r--r--app/services/members/approve_access_request_service.rb21
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml4
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb8
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--lib/api/internal.rb13
-rw-r--r--lib/tasks/teaspoon.rake38
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es678
-rw-r--r--spec/models/members/project_member_spec.rb4
-rw-r--r--spec/models/repository_spec.rb10
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb63
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/support/taskable_shared_examples.rb6
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb55
34 files changed, 348 insertions, 73 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6e3ab842ac..db780469f1d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add hover to trash icon in notes !7008 (blackst0ne)
- Only show one error message for an invalid email !5905 (lycoperdon)
- Fix sidekiq stats in admin area (blackst0ne)
+ - Created cycle analytics bundle JavaScript file
- API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954
- Stop unauthorized users dragging on milestone page (blackst0ne)
@@ -22,25 +23,30 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix filtering of milestones with quotes in title (airatshigapov)
- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
- Update mail_room and enable sentinel support to Reply By Email (!7101)
+ - Add task completion status in Issues and Merge Requests tabs: "X of Y tasks completed" (!6527, @gmesalazar)
- Simpler arguments passed to named_route on toggle_award_url helper method
- Fix typo in framework css class. !7086 (Daniel Voogsgerd)
- New issue board list dropdown stays open after adding a new list
- Fix: Backup restore doesn't clear cache
+ - Optimize Event queries by removing default order
- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
+ - Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld)
- Replace jquery.cookie plugin with js.cookie !7085
- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
- Show full status link on MR & commit pipelines
- Fix documents and comments on Build API `scope`
- - Fix applying labels for GitHub-imported MRs
- - Fix importing MR comments from GitHub
- - Modify GitHub importer to be retryable
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
- Shortened merge request modal to let clipboard button not overlap
+ - In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
## 8.13.2
- Fix builds dropdown overlapping bug !7124
+ - Fix applying labels for GitHub-imported MRs !7139
+ - Fix importing MR comments from GitHub !7139
+ - Modify GitHub importer to be retryable !7003
- Fix and improve `Sortable.highest_label_priority`
+ - Fixed sticky merge request tabs when sidebar is pinned
## 8.13.1 (2016-10-25)
- Fix branch protection API. !6215
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4f2c1d15f6d..fcdb2e109f6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.6
+4.0.0
diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index 331f0209888..331f0209888 100644
--- a/app/assets/javascripts/cycle_analytics.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 1d9f641836f..98e43c4d088 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -239,6 +239,7 @@
this.fullData = this.options.data;
currentIndex = -1;
this.parseData(this.options.data);
+ this.focusTextInput();
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
dataType: this.options.dataType,
@@ -247,6 +248,7 @@
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
+ _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input) {
return _this.filter.input.trigger('input');
}
@@ -452,9 +454,8 @@
contentHtml = $('.dropdown-content', this.dropdown).html();
if (this.remote && contentHtml === "") {
this.remote.execute();
- }
- if (this.options.filterable) {
- this.filterInput.focus();
+ } else {
+ this.focusTextInput();
}
if (this.options.showMenuAbove) {
@@ -691,6 +692,10 @@
return selectedObject;
};
+ GitLabDropdown.prototype.focusTextInput = function() {
+ if (this.options.filterable) { this.filterInput.focus() }
+ }
+
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
// Create hidden input for form
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index e83dae2bb3c..67ace697936 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -95,7 +95,11 @@
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
- data: patchData
+ data: patchData,
+ success: function(issue) {
+ document.querySelector('#task_status').innerText = issue.task_status;
+ document.querySelector('#task_status_short').innerText = issue.task_status_short;
+ }
});
// TODO (rspeicher): Make the issue description inline-editable like a note so
// that we can re-use its form here
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index a0bce6ef381..d3bd1e846c1 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -97,7 +97,11 @@
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
- data: patchData
+ data: patchData,
+ success: function(mergeRequest) {
+ document.querySelector('#task_status').innerText = mergeRequest.task_status;
+ document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
+ }
});
// TODO (rspeicher): Make the merge request description inline-editable like a
// note so that we can re-use its form here
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 1d8e64a0e4b..c54f7b27575 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -164,6 +164,18 @@
padding-left: $sidebar_width;
}
}
+
+ .merge-request-tabs-holder.affix {
+ @media (min-width: $sidebar-breakpoint) {
+ left: $sidebar_width;
+ }
+ }
+
+ &.right-sidebar-expanded {
+ .line-resolve-all-container {
+ display: none;
+ }
+ }
}
header.header-sidebar-pinned {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3c60db40ffe..a8e8bbcb208 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -573,8 +573,7 @@
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
- &::after,
- &::before {
+ &::after {
border: none;
}
}
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cb649264146..3f1a1d1c511 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -112,7 +112,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
format.json do
- render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
+ render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2ee53f7ceda..30f1cf4e5be 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -278,7 +278,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request])
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
else
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03b2db1bc91..ef6cfb235a9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -71,6 +71,14 @@ module IssuablesHelper
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
+
+ if issuable.tasks?
+ output << "&ensp;".html_safe
+ output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs")
+ output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg")
+ end
+
+ output
end
def issuable_todo(issuable)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 17c3b526c97..613444e0d70 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -12,6 +12,7 @@ module Issuable
include Subscribable
include StripAttribute
include Awardable
+ include Taskable
included do
cache_markdown_field :title, pipeline: :single_line
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index a3ac577cf3e..ebc75100a54 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -53,10 +53,22 @@ module Taskable
# Return a string that describes the current state of this Taskable's task
# list items, e.g. "12 of 20 tasks completed"
- def task_status
+ def task_status(short: false)
return '' if description.blank?
+ prep, completed = if short
+ ['/', '']
+ else
+ [' of ', ' completed']
+ end
+
sum = tasks.summary
- "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
+ "#{sum.complete_count}#{prep}#{sum.item_count} #{'task'.pluralize(sum.item_count)}#{completed}"
+ end
+
+ # Return a short string that describes the current state of this Taskable's
+ # task list items -- for small screens
+ def task_status_short
+ task_status(short: true)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 3993b35f96d..43e67069b70 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,6 +1,6 @@
class Event < ActiveRecord::Base
include Sortable
- default_scope { where.not(author_id: nil) }
+ default_scope { reorder(nil).where.not(author_id: nil) }
CREATED = 1
UPDATED = 2
diff --git a/app/models/issue.rb b/app/models/issue.rb
index e356fe06363..4f02b02c488 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,7 +5,6 @@ class Issue < ActiveRecord::Base
include Issuable
include Referable
include Sortable
- include Taskable
include Spammable
include FasterCacheKeys
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 18657c3e1c8..7712d5783e0 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -17,4 +17,10 @@ class LfsObject < ActiveRecord::Base
def project_allowed_access?(project)
projects.exists?(storage_project(project).id)
end
+
+ def self.destroy_unreferenced
+ joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
+ .where(lfs_objects_projects: { id: nil })
+ .destroy_all
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4872f8b8649..0397c57f935 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Referable
include Sortable
- include Taskable
include Importable
belongs_to :target_project, class_name: "Project"
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 70661387dc2..30be7262438 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -11,6 +11,20 @@ class Repository
attr_accessor :path_with_namespace, :project
+ def self.storages
+ Gitlab.config.repositories.storages
+ end
+
+ def self.remove_storage_from_path(repo_path)
+ storages.find do |_, storage_path|
+ if repo_path.start_with?(storage_path)
+ return repo_path.sub(storage_path, '')
+ end
+ end
+
+ repo_path
+ end
+
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 416aee2ab51..c13f289f61e 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -4,17 +4,25 @@ module Members
attr_accessor :source
+ # source - The source object that respond to `#requesters` (i.g. project or group)
+ # current_user - The user that performs the access request approval
+ # params - A hash of parameters
+ # :user_id - User ID used to retrieve the access requester
+ # :id - Member ID used to retrieve the access requester
+ # :access_level - Optional access level set when the request is accepted
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
- @params = params
+ @params = params.slice(:user_id, :id, :access_level)
end
- def execute
+ # opts - A hash of options
+ # :force - Bypass permission check: current_user can be nil in that case
+ def execute(opts = {})
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)
+ raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
@@ -24,8 +32,11 @@ module Members
private
- def can_update_access_requester?(access_requester)
- access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ def can_update_access_requester?(access_requester, opts = {})
+ access_requester && (
+ opts[:force] ||
+ can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ )
end
end
end
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index b647882efa0..247d612ba6f 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,5 +1,9 @@
- @no_container = true
- page_title "Cycle Analytics"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js')
+
= render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
new file mode 100644
index 00000000000..b80f131d5f7
--- /dev/null
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -0,0 +1,8 @@
+class RemoveUnreferencedLfsObjectsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform
+ LfsObject.destroy_unreferenced
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 92c8467e7f4..946b632b0e8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -91,6 +91,7 @@ module Gitlab
config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
+ config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index efe0ac9c965..9fec2ad6bf7 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -307,6 +307,9 @@ Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWork
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
#
# GitLab Shell
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 79bbe8421c6..a313c31e7ee 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -242,10 +242,10 @@ docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
- docker push registry.example.com/group/project:latest
```
-You have to use the credentials of the special `gitlab-ci-token` user with its
-password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
-to your project. This allows you to automate building and deployment of your
-Docker images.
+You have to use the special `gitlab-ci-token` user created for you in order to
+push to the Registry connected to your project. Its password is provided in the
+`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
+of your Docker images.
Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
including two tests that run in parallel. The build is stored in the container
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 9a5d1ece070..ccf181402f9 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -17,15 +17,20 @@ module API
#
helpers do
+ def project_path
+ @project_path ||= begin
+ project_path = params[:project].sub(/\.git\z/, '')
+ Repository.remove_storage_from_path(project_path)
+ end
+ end
+
def wiki?
- @wiki ||= params[:project].end_with?('.wiki') &&
- !Project.find_with_namespace(params[:project])
+ @wiki ||= project_path.end_with?('.wiki') &&
+ !Project.find_with_namespace(project_path)
end
def project
@project ||= begin
- project_path = params[:project]
-
# Check for *.wiki repositories.
# Strip out the .wiki from the pathname before finding the
# project. This applies the correct project permissions to
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
index 156fa90537d..08caedd7ff3 100644
--- a/lib/tasks/teaspoon.rake
+++ b/lib/tasks/teaspoon.rake
@@ -1,23 +1,25 @@
-Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
+unless Rails.env.production?
+ Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
-namespace :teaspoon do
- desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
- ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
- t.rspec_opts = '--format documentation'
- end
+ namespace :teaspoon do
+ desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures) do |t|
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.rspec_opts = '--format documentation'
+ end
- desc 'GitLab | Teaspoon | Run JavaScript tests'
- task :tests do
- require "teaspoon/console"
- options = {}
- abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
+ desc 'GitLab | Teaspoon | Run JavaScript tests'
+ task :tests do
+ require "teaspoon/console"
+ options = {}
+ abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
+ end
end
-end
-desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
-task :teaspoon do
- Rake::Task['teaspoon:fixtures'].invoke
- Rake::Task['teaspoon:tests'].invoke
+ desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
+ task :teaspoon do
+ Rake::Task['teaspoon:fixtures'].invoke
+ Rake::Task['teaspoon:tests'].invoke
+ end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 4e3ef5dc6fa..7c5f33c63b8 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::MilestonesController do
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
expect(response).to be_success
- expect(Event.first.action).to eq(Event::DESTROYED)
+ expect(Event.recent.first.action).to eq(Event::DESTROYED)
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
issue.reload
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index 685e662edd3..8ba238018cd 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -7,6 +7,7 @@
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
@@ -17,6 +18,8 @@
ESC: 27
};
+ let remoteCallback;
+
let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
@@ -33,18 +36,19 @@
}
};
+ let remoteMock = function remoteMock(data, term, callback) {
+ remoteCallback = callback.bind({}, data);
+ }
+
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
- beforeEach(() => {
- fixture.load('gl_dropdown.html');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = fixture.load('projects.json')[0];
+ function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
- data: this.projectsData,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
text: (project) => {
(project.name_with_namespace || project.name);
},
@@ -52,6 +56,13 @@
project.id;
}
});
+ }
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
});
afterEach(() => {
@@ -60,6 +71,7 @@
});
it('should open on click', () => {
+ initDropDown.call(this, false);
expect(this.dropdownContainerElement).not.toHaveClass('open');
this.dropdownButtonElement.click();
expect(this.dropdownContainerElement).toHaveClass('open');
@@ -67,26 +79,27 @@
describe('that is open', () => {
beforeEach(() => {
+ initDropDown.call(this, false, false);
this.dropdownButtonElement.click();
});
it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
});
@@ -98,7 +111,7 @@
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
- let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
let linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
@@ -116,5 +129,42 @@
expect(this.dropdownContainerElement).not.toHaveClass('open');
});
});
+
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should not focus search input while remote task is not complete', ()=> {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', ()=> {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time', ()=> {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', ()=> {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
});
})();
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f6b2ec5ae31..68f72f5c86e 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -57,12 +57,12 @@ describe ProjectMember, models: true do
it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: Time.now - 6.days)
expired.destroy
- expect(Event.first.action).to eq(Event::EXPIRED)
+ expect(Event.recent.first.action).to eq(Event::EXPIRED)
end
it "creates a left event when left due to leave" do
master.destroy
- expect(Event.first.action).to eq(Event::LEFT)
+ expect(Event.recent.first.action).to eq(Event::LEFT)
end
it "destroys itself and delete associated todos" do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 19b3e7e470d..04b7d19d414 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1472,4 +1472,14 @@ describe Repository, models: true do
end.to raise_error(Repository::CommitError)
end
end
+
+ describe '#remove_storage_from_path' do
+ let(:storage_path) { project.repository_storage_path }
+ let(:project_path) { project.path_with_namespace }
+ let(:full_path) { File.join(storage_path, project_path) }
+
+ it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
+ it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
+ it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
+ end
end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 03e296259f9..7b090343a3e 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
+ let(:opts) { {} }
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)
+ expect { described_class.new(source, user, params).execute(opts) }.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)
+ expect { described_class.new(source, user, params).execute(opts) }.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)
+ expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
- member = described_class.new(source, user, params).execute
+ member = described_class.new(source, user, params).execute(opts)
expect(member).to be_a "#{source.class}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 } }
+ let(:params2) { params.merge(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
+ member = described_class.new(source, user, params2).execute(opts)
expect(member.access_level).to eq Gitlab::Access::MASTER
end
@@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do
end
let(:params) { { user_id: access_requester.id } }
+ context 'when current user is nil' do
+ let(:user) { nil }
+
+ context 'and :force option is not given' 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 'and :force option is false' do
+ let(:opts) { { force: false } }
+
+ 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 'and :force option is true' do
+ let(:opts) { { force: true } }
+
+ 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
+ end
+
+ context 'and :force param is true' do
+ let(:params) { { user_id: access_requester.id, force: true } }
+
+ 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
+ end
+
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 }
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 5d400299be0..92b84308f73 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -18,7 +18,7 @@ describe Milestones::CloseService, services: true do
it { expect(milestone).to be_closed }
describe :event do
- let(:event) { Event.first }
+ let(:event) { Event.recent.first }
it { expect(event.milestone).to be_truthy }
it { expect(event.target).to eq(milestone) }
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 201614e45a4..ad1c783df4d 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -17,6 +17,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('2 of')
expect(subject.task_status).to match('5 tasks completed')
+ expect(subject.task_status_short).to match('2/')
+ expect(subject.task_status_short).to match('5 tasks')
end
describe '#tasks?' do
@@ -41,6 +43,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('0 of')
expect(subject.task_status).to match('1 task completed')
+ expect(subject.task_status_short).to match('0/')
+ expect(subject.task_status_short).to match('1 task')
end
end
@@ -54,6 +58,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('1 of')
expect(subject.task_status).to match('1 task completed')
+ expect(subject.task_status_short).to match('1/')
+ expect(subject.task_status_short).to match('1 task')
end
end
end
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
new file mode 100644
index 00000000000..6d42946de38
--- /dev/null
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe RemoveUnreferencedLfsObjectsWorker do
+ let(:worker) { RemoveUnreferencedLfsObjectsWorker.new }
+
+ describe '#perform' do
+ let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
+ let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
+ let!(:project1) { create(:empty_project, lfs_enabled: true) }
+ let!(:project2) { create(:empty_project, lfs_enabled: true) }
+ let!(:referenced_lfs_object1) { create(:lfs_object, oid: '3') }
+ let!(:referenced_lfs_object2) { create(:lfs_object, oid: '4') }
+ let!(:lfs_objects_project1_1) do
+ create(:lfs_objects_project,
+ project: project1,
+ lfs_object: referenced_lfs_object1
+ )
+ end
+ let!(:lfs_objects_project2_1) do
+ create(:lfs_objects_project,
+ project: project2,
+ lfs_object: referenced_lfs_object1
+ )
+ end
+ let!(:lfs_objects_project1_2) do
+ create(:lfs_objects_project,
+ project: project1,
+ lfs_object: referenced_lfs_object2
+ )
+ end
+
+ it 'removes unreferenced lfs objects' do
+ worker.perform
+
+ expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
+ expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
+ end
+
+ it 'leaves referenced lfs objects' do
+ worker.perform
+
+ expect(referenced_lfs_object1.reload).to be_present
+ expect(referenced_lfs_object2.reload).to be_present
+ end
+
+ it 'removes unreferenced lfs objects after project removal' do
+ project1.destroy
+
+ worker.perform
+
+ expect(referenced_lfs_object1.reload).to be_present
+ expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
+ end
+ end
+end