Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--CHANGELOG14
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock9
-rw-r--r--app/assets/javascripts/application.js.coffee29
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee5
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee6
-rw-r--r--app/assets/javascripts/profile.js.coffee48
-rw-r--r--app/assets/javascripts/sidebar.js.coffee18
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss20
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/pages/profile.scss54
-rw-r--r--app/controllers/concerns/continue_params.rb13
-rw-r--r--app/controllers/groups_controller.rb10
-rw-r--r--app/controllers/oauth/applications_controller.rb24
-rw-r--r--app/controllers/profiles_controller.rb10
-rw-r--r--app/controllers/projects/forks_controller.rb13
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/imports_controller.rb12
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb24
-rw-r--r--app/finders/issuable_finder.rb7
-rw-r--r--app/finders/projects_finder.rb8
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb10
-rw-r--r--app/helpers/events_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/milestones_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb5
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/commit.rb34
-rw-r--r--app/models/ci/runner.rb20
-rw-r--r--app/models/commit_status.rb18
-rw-r--r--app/models/concerns/issuable.rb21
-rw-r--r--app/models/group.rb14
-rw-r--r--app/models/key.rb3
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/milestone.rb18
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/note.rb52
-rw-r--r--app/models/project.rb53
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_team.rb52
-rw-r--r--app/models/snippet.rb24
-rw-r--r--app/models/user.rb27
-rw-r--r--app/services/ci/image_for_build_service.rb2
-rw-r--r--app/services/create_commit_builds_service.rb1
-rw-r--r--app/services/git_push_service.rb11
-rw-r--r--app/services/projects/housekeeping_service.rb27
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/project_service.rb2
-rw-r--r--app/services/search/snippet_service.rb5
-rw-r--r--app/uploaders/avatar_uploader.rb11
-rw-r--r--app/views/admin/builds/_build.html.haml21
-rw-r--r--app/views/admin/groups/show.html.haml16
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml8
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml31
-rw-r--r--app/views/doorkeeper/applications/index.html.haml98
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event_last_push.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml12
-rw-r--r--app/views/groups/_projects.html.haml13
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/groups/activity.html.haml9
-rw-r--r--app/views/groups/edit.html.haml9
-rw-r--r--app/views/groups/show.html.haml43
-rw-r--r--app/views/layouts/nav/_group.html.haml7
-rw-r--r--app/views/layouts/nav/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml6
-rw-r--r--app/views/profiles/applications.html.haml70
-rw-r--r--app/views/profiles/show.html.haml19
-rw-r--r--app/views/projects/builds/index.html.haml3
-rw-r--r--app/views/projects/builds/show.html.haml19
-rw-r--r--app/views/projects/ci/builds/_build.html.haml76
-rw-r--r--app/views/projects/commit/_builds.html.haml7
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml79
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml58
-rw-r--r--app/views/projects/go_import.html.haml5
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/devise.rb10
-rw-r--r--config/initializers/go_get.rb1
-rw-r--r--config/initializers/mysql_ignore_postgresql_options.rb49
-rw-r--r--config/initializers/postgresql_opclasses_support.rb188
-rw-r--r--config/routes.rb6
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb10
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb5
-rw-r--r--db/migrate/20150930110012_add_group_share_lock.rb5
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb53
-rw-r--r--db/migrate/20160307221555_disallow_blank_line_code_on_note.rb9
-rw-r--r--db/migrate/20160314143402_projects_add_pushes_since_gc.rb5
-rw-r--r--db/schema.rb45
-rw-r--r--doc/api/builds.md7
-rw-r--r--doc/api/notes.md1
-rw-r--r--doc/api/project_snippets.md1
-rw-r--r--doc/api/projects.md14
-rw-r--r--doc/ci/README.md23
-rw-r--r--doc/ci/enable_or_disable_ci.md2
-rw-r--r--doc/ci/examples/README.md16
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md2
-rw-r--r--doc/ci/quick_start/README.md11
-rw-r--r--doc/ci/yaml/README.md298
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/requirements.md11
-rw-r--r--doc/update/8.5-to-8.6.md164
-rw-r--r--doc/web_hooks/web_hooks.md1
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/groups/max_access_level.pngbin0 -> 135354 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin0 -> 118382 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin0 -> 118868 bytes
-rw-r--r--doc/workflow/share_projects_with_other_groups.md30
-rw-r--r--doc/workflow/share_with_group.md13
-rw-r--r--doc/workflow/share_with_group.pngbin0 -> 53784 bytes
-rw-r--r--features/admin/groups.feature5
-rw-r--r--features/groups.feature4
-rw-r--r--features/profile/profile.feature3
-rw-r--r--features/project/group_links.feature16
-rw-r--r--features/project/merge_requests.feature9
-rw-r--r--features/project/network_graph.feature3
-rw-r--r--features/project/team_management.feature5
-rw-r--r--features/steps/admin/groups.rb19
-rw-r--r--features/steps/dashboard/issues.rb21
-rw-r--r--features/steps/dashboard/merge_requests.rb20
-rw-r--r--features/steps/profile/profile.rb30
-rw-r--r--features/steps/project/issues/award_emoji.rb5
-rw-r--r--features/steps/project/merge_requests.rb20
-rw-r--r--features/steps/project/network_graph.rb9
-rw-r--r--features/steps/project/project_group_links.rb50
-rw-r--r--features/steps/project/team_management.rb19
-rw-r--r--features/steps/shared/builds.rb2
-rw-r--r--features/steps/shared/issuable.rb4
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--lib/api/entities.rb14
-rw-r--r--lib/api/projects.rb27
-rw-r--r--lib/banzai/filter/sanitization_filter.rb9
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb27
-rw-r--r--lib/gitlab/devise_failure.rb23
-rw-r--r--lib/gitlab/github_import/importer.rb11
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb10
-rw-r--r--lib/gitlab/middleware/go.rb50
-rw-r--r--lib/gitlab/project_search_results.rb10
-rw-r--r--lib/gitlab/search_results.rb25
-rw-r--r--lib/gitlab/snippet_search_results.rb10
-rw-r--r--spec/controllers/namespaces_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb4
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb37
-rw-r--r--spec/controllers/projects_controller_spec.rb13
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/project_group_links.rb6
-rw-r--r--spec/factories/users.rb7
-rw-r--r--spec/finders/projects_finder_spec.rb34
-rw-r--r--spec/helpers/application_helper_spec.rb6
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb20
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb84
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb64
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb30
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/search_results_spec.rb55
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb25
-rw-r--r--spec/mailers/emails/profile_spec.rb6
-rw-r--r--spec/models/build_spec.rb66
-rw-r--r--spec/models/ci/commit_spec.rb44
-rw-r--r--spec/models/ci/runner_spec.rb28
-rw-r--r--spec/models/concerns/issuable_spec.rb47
-rw-r--r--spec/models/group_spec.rb26
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/models/milestone_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb29
-rw-r--r--spec/models/note_spec.rb82
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_spec.rb73
-rw-r--r--spec/models/project_team_spec.rb44
-rw-r--r--spec/models/snippet_spec.rb44
-rw-r--r--spec/models/user_spec.rb72
-rw-r--r--spec/requests/api/project_snippets_spec.rb18
-rw-r--r--spec/requests/api/projects_spec.rb36
-rw-r--r--spec/services/git_push_service_spec.rb39
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb48
-rwxr-xr-xvendor/assets/javascripts/cropper.js2972
-rwxr-xr-xvendor/assets/stylesheets/cropper.css379
199 files changed, 3230 insertions, 4411 deletions
diff --git a/.gitignore b/.gitignore
index 1eb785451f4..8f861d76a37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
+.byebug_history
Vagrantfile
backups/*
config/aws.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b5f53725f95..2ad63548d78 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -156,13 +156,14 @@ flay:
bundler:audit:
stage: test
+ only:
+ - master
script:
- "bundle exec bundle-audit update"
- - "bundle exec bundle-audit check"
+ - "bundle exec bundle-audit check --ignore OSVDB-115941"
tags:
- ruby
- mysql
- allow_failure: true
# Ruby 2.2 jobs
@@ -170,7 +171,7 @@ spec:feature:ruby22:
stage: test
image: ruby:2.2
only:
- - master
+ - master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
diff --git a/CHANGELOG b/CHANGELOG
index 6348a7e3487..015efa05c6a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,13 +1,15 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
+ - Bump gitlab_git to 9.0.3 (Stan Hu)
+ - Support Golang subpackage fetching (Stan Hu)
+ - Bump Capybara gem to 2.6.2 (Stan Hu)
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki
- - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
@@ -17,22 +19,32 @@ v 8.6.0 (unreleased)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
+ - Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
+ - Ignore jobs that start with `.` (hidden jobs)
+ - Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
+ - Refactor and greatly improve search performance
- Add support for cross-project label references
+ - Ensure "new SSH key" email do not ends up as dead Sidekiq jobs
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
+ - Allow to define on which builds the current one depends on
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
+ - Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
+ - Move group activity to separate page
+ - Continue parameters are checked to ensure redirection goes to the same instance
v 8.5.5
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
+ - Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4
- Do not cache requests for badges (including builds badge)
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index a04abec9149..bc02b8685c1 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.10
+2.6.11
diff --git a/Gemfile b/Gemfile
index d022ac96bf4..a0e8e796627 100644
--- a/Gemfile
+++ b/Gemfile
@@ -77,9 +77,6 @@ gem "haml-rails", '~> 0.9.0'
# Files attachments
gem "carrierwave", '~> 0.10.0'
-# Image editing
-gem "mini_magick", '~> 4.4.0'
-
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -273,7 +270,7 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
- gem 'capybara', '~> 2.4.0'
+ gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index b69980af2ba..f4f5649eb75 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -108,7 +108,8 @@ GEM
thor (~> 0.18)
byebug (8.2.1)
cal-heatmap-rails (3.5.1)
- capybara (2.4.4)
+ capybara (2.6.2)
+ addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
@@ -358,7 +359,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (9.0.1)
+ gitlab_git (9.0.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -468,7 +469,6 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mimemagic (0.3.0)
- mini_magick (4.4.0)
mini_portile2 (2.0.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
@@ -904,7 +904,7 @@ DEPENDENCIES
bundler-audit
byebug
cal-heatmap-rails (~> 3.5.0)
- capybara (~> 2.4.0)
+ capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
@@ -959,7 +959,6 @@ DEPENDENCIES
loofah (~> 2.0.3)
mail_room (~> 0.6.1)
method_source (~> 0.8)
- mini_magick (~> 4.4.0)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 1212e89975b..d415bbd3476 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -42,7 +42,6 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
-#= require cropper.js
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -108,6 +107,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -256,35 +257,14 @@ $ ->
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
- bootstrapBreakpoint = undefined;
- checkBootstrapBreakpoints = ->
- if $('.device-xs').is(':visible')
- bootstrapBreakpoint = "xs"
- else if $('.device-sm').is(':visible')
- bootstrapBreakpoint = "sm"
- else if $('.device-md').is(':visible')
- bootstrapBreakpoint = "md"
- else if $('.device-lg').is(':visible')
- bootstrapBreakpoint = "lg"
-
- setBootstrapBreakpoints = ->
- if $('.device-xs').length
- return
-
- $("body")
- .append('<div class="device-xs visible-xs"></div>'+
- '<div class="device-sm visible-sm"></div>'+
- '<div class="device-md visible-md"></div>'+
- '<div class="device-lg visible-lg"></div>')
- checkBootstrapBreakpoints()
-
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
- checkBootstrapBreakpoints()
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
@@ -293,6 +273,5 @@ $ ->
.on "resize", (e) ->
fitSidebarForSize()
- setBootstrapBreakpoints()
checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 54b28f2dd8d..1be86e3b820 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
- when 'groups:show'
+ when 'groups:activity'
new Activities()
+ when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
@@ -103,6 +104,8 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 8e1449bc59c..4f038477755 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -238,18 +238,20 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+ if !value?
+ field.remove()
+
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
- field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
- if value
+ if value?
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 59d44c30bee..20f87440551 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -17,52 +17,14 @@ class @Profile
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
- # Avatar management
-
- $avatarInput = $('.js-user-avatar-input')
- $filename = $('.js-avatar-filename')
- $modalCrop = $('.modal-profile-crop')
- $modalCropImg = $('.modal-profile-crop-image')
-
- $('.js-choose-user-avatar-button').on "click", ->
- $form = $(this).closest("form")
- $form.find(".js-user-avatar-input").click()
-
- $modalCrop.on 'shown.bs.modal', ->
- setTimeout ( -> # The cropper must be asynchronously initialized
- $modalCropImg.cropper
- aspectRatio: 1
- modal: false
- scalable: false
- rotatable: false
- zoomable: false
-
- crop: (event) ->
- ['x', 'y'].forEach (key) ->
- $("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
- $("#user_avatar_crop_size").val(Math.floor(event.width))
- ), 0
-
- $modalCrop.on 'hidden.bs.modal', ->
- $modalCropImg.attr('src', '').cropper('destroy')
- $avatarInput.val('')
- $filename.text($filename.data('label'))
-
- $('.js-upload-user-avatar').on 'click', ->
- $('.edit-user').submit()
+ $('.js-choose-user-avatar-button').bind "click", ->
+ form = $(this).closest("form")
+ form.find(".js-user-avatar-input").click()
- $avatarInput.on "change", ->
+ $('.js-user-avatar-input').bind "change", ->
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
- $filename.data('label', $filename.text()).text(filename)
-
- reader = new FileReader
-
- reader.onload = (event) ->
- $modalCrop.modal('show')
- $modalCropImg.attr('src', event.target.result)
-
- fileData = reader.readAsDataURL(this.files[0])
+ form.find(".js-avatar-filename").text(filename)
$ ->
# Extract the SSH Key title from its comment
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index cff309c5972..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,8 +1,7 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+expanded = 'page-sidebar-expanded'
+toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar();
), 300
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index e2d590f4df4..2d301d21ab9 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,7 +9,6 @@
*= require_self
*= require dropzone/basic
*= require cal-heatmap
- *= require cropper.css
*/
/*
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 6edabe20136..d20b77ffae9 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -120,6 +120,10 @@
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
+
+ &.username:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.cover-controls {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index e624982c5c9..4c4033e3ae7 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed {
- margin-left: $sidebar_collapsed_width;
- }
-
- .header-expanded {
- margin-left: $sidebar_width;
- }
-}
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
-@media(min-width: $screen-md-max) {
- .header-collapsed {
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
+}
+
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 368bbfe5355..1d5000fe388 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -41,12 +41,6 @@
transition: $transition;
}
-@mixin transform($transform) {
- -webkit-transform: $transform;
- -ms-transform: $transform;
- transform: $transform;
-}
-
/**
* Prefilled mixins
* Mixins with fixed values
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7de874c8bcd..b2fbc95e043 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -63,7 +63,7 @@
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-sm-max) {
width: 100%;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 6b382e4b1b2..26df9acd2ae 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -34,12 +34,12 @@
@media (min-width: $screen-sm-min) {
padding-right: $gutter_width;
}
-
+
}
}
.sidebar-wrapper {
- z-index: 99;
+ z-index: 999;
background: $background-color;
}
@@ -203,7 +203,11 @@
}
@mixin expanded-sidebar {
- padding-left: $sidebar_width;
+ padding-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
+ }
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0261c384a58..d491d01a3cf 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -103,6 +103,10 @@ $border-red-dark: #CA264F;
$help-well-bg: #FAFAFA;
$help-well-border: #E5E5E5;
+$warning-message-bg: #FBF2D9;
+$warning-message-color: #9E8E60;
+$warning-message-border: #F0E2BB;
+
/* header */
$light-grey-header: #faf9f9;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 248c56e459d..ecfe0e37c85 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -109,42 +109,6 @@
}
}
-.modal-profile-crop {
- .modal-dialog {
- width: 500px;
- }
-
- .modal-body {
- p {
- display: table;
- margin: auto;
- overflow: hidden;
- }
-
- img {
- display: block;
- max-width: 400px;
- max-height: 400px;
- }
-
- .cropper-bg {
- background: none;
- }
-
- .cropper-crop-box {
- box-sizing: content-box;
- border: 999px solid transparentize(#ccc, 0.5);
- @include transform(translate(-999px, -999px));
- }
- }
-}
-
-@media (max-width: 520px) {
- .modal-profile-crop .modal-dialog {
- width: auto;
- }
-}
-
.key-list-item {
.key-list-item-info {
@media (min-width: $screen-sm-min) {
@@ -215,3 +179,21 @@
color: $provider-btn-not-active-color;
}
}
+
+.profile-settings-message {
+ line-height: 32px;
+ color: $warning-message-color;
+ background-color: $warning-message-bg;
+ border: 1px solid $warning-message-border;
+ border-radius: $border-radius-base;
+}
+
+.oauth-applications {
+ form {
+ display: inline-block;
+ }
+
+ .last-heading {
+ width: 105px;
+ }
+}
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
new file mode 100644
index 00000000000..0a995c45bdf
--- /dev/null
+++ b/app/controllers/concerns/continue_params.rb
@@ -0,0 +1,13 @@
+module ContinueParams
+ extend ActiveSupport::Concern
+
+ def continue_params
+ continue_params = params[:continue]
+ return nil unless continue_params
+
+ continue_params = continue_params.permit(:to, :notice, :notice_now)
+ return unless continue_params[:to] && continue_params[:to].start_with?('/')
+
+ continue_params
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f05c29e9974..06c5c8be9a5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
- before_action :event_filter, only: [:show, :events]
+ before_action :event_filter, only: [:activity]
layout :determine_layout
@@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @shared_projects = @group.shared_projects
+
respond_to do |format|
format.html
@@ -62,8 +64,10 @@ class GroupsController < Groups::ApplicationController
end
end
- def events
+ def activity
respond_to do |format|
+ format.html
+
format.json do
load_events
pager_json("events/_events", @events.count)
@@ -131,7 +135,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
end
def load_events
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index dc22101cd5e..d1e4ac10f6c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile'
def index
- head :forbidden and return
+ set_index_vars
end
def create
@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
else
- render :new
+ set_index_vars
+ render :index
end
end
- def destroy
- if @application.destroy
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
- end
-
- redirect_to applications_profile_url
- end
-
private
def verify_user_oauth_applications_enabled
@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url
end
+ def set_index_vars
+ @applications = current_user.oauth_applications
+ @authorized_tokens = current_user.oauth_authorized_tokens
+ @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
+ @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
+
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
+ end
+
+ # Override Doorkeeper to scope to the current user
def set_application
@application = current_user.oauth_applications.find(params[:id])
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index fa7a1148961..32fca6b838e 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
def show
end
- def applications
- @applications = current_user.oauth_applications
- @authorized_tokens = current_user.oauth_authorized_tokens
- @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
- @authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
- end
-
def update
user_params.except!(:email) if @user.ldap_user?
@@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController
def user_params
params.require(:user).permit(
- :avatar_crop_x,
- :avatar_crop_y,
- :avatar_crop_size,
:avatar,
:bio,
:email,
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 7b202f3862f..a1b8632df98 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,4 +1,6 @@
class Projects::ForksController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
-
- private
-
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+class Projects::GroupLinksController < Projects::ApplicationController
+ layout 'project_settings'
+ before_action :authorize_admin_project!
+
+ def index
+ @group_links = project.project_group_links.all
+ end
+
+ def create
+ link = project.project_group_links.new
+ link.group_id = params[:link_group_id]
+ link.group_access = params[:link_group_access]
+ link.save
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def destroy
+ project.project_group_links.find(params[:id]).destroy
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 196996f1752..7756f0f0ed3 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,4 +1,6 @@
class Projects::ImportsController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create]
@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
private
- def continue_params
- continue_params = params[:continue]
-
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
-
def finished_notice
if @project.forked?
'The project was successfully forked.'
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
+ @project_group_links = @project.project_group_links
end
def create
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index aea08ecce3e..36f37221c58 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,7 +1,6 @@
class ProjectsController < ApplicationController
include ExtractsPath
- prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
@@ -173,10 +172,15 @@ class ProjectsController < ApplicationController
def housekeeping
::Projects::HousekeepingService.new(@project).execute
- respond_to do |format|
- flash[:notice] = "Housekeeping successfully started."
- format.html { redirect_to project_path(@project) }
- end
+ redirect_to(
+ project_path(@project),
+ notice: "Housekeeping successfully started"
+ )
+ rescue ::Projects::HousekeepingService::LeaseTaken => ex
+ redirect_to(
+ edit_project_path(@project),
+ alert: ex.to_s
+ )
end
def toggle_star
@@ -242,16 +246,6 @@ class ProjectsController < ApplicationController
end
end
- def render_go_import
- return unless params["go-get"] == "1"
-
- @namespace = params[:namespace_id]
- @id = params[:project_id] || params[:id]
- @id = @id.gsub(/\.git\Z/, "")
-
- render "go_import", layout: false
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c88a420b412..19e8c7a92be 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -244,10 +244,17 @@ class IssuableFinder
items
end
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == '#upcoming'
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil])
+ elsif filter_by_upcoming_milestone?
+ upcoming = Milestone.where(project_id: projects).upcoming
+ items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3b4e0362e04..2c55f088594 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -43,7 +43,8 @@ class ProjectsFinder
if current_user
[
group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only
+ group.projects.public_and_internal_only,
+ group.shared_projects.visible_to_user(current_user)
]
else
[group.projects.public_only]
@@ -52,7 +53,10 @@ class ProjectsFinder
def all_projects(current_user)
if current_user
- [current_user.authorized_projects, public_and_internal_projects]
+ [
+ current_user.authorized_projects,
+ public_and_internal_projects
+ ]
else
[Project.public_only]
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 368969c6472..d1b1c61b710 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email.try(:downcase))
+ user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index f20779f2fbb..8b1575d5e0c 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status)
end
- def ci_status_with_icon(status)
- content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ def ci_status_with_icon(status, target = nil)
+ content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ klass = "ci-status ci-#{status}"
+ if target
+ link_to content, target, class: klass
+ else
+ content_tag :span, content, class: klass
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index e5fcaab9551..37a888d9c60 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -3,7 +3,7 @@ module EventsHelper
author = event.author
if author
- link_to author.name, user_path(author.username)
+ link_to author.name, user_path(author.username), title: h(author.name)
else
event.author_name
end
@@ -159,7 +159,7 @@ module EventsHelper
link_to(
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
- anchor: dom_id(event.target)),
+ anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id"
) do
"#{event.note_target_type} #{event.note_short_commit_id}"
@@ -167,7 +167,7 @@ module EventsHelper
elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace,
event.project,
- event.note_target)) do
+ event.note_target), title: h(event.project.name)) do
"#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
else
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 91a3aa371ef..2dfeddf7368 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -31,7 +31,11 @@ module IssuablesHelper
end
def issuable_state_scope(issuable)
- issuable.open? ? :opened : :closed
+ if issuable.respond_to?(:merged?) && issuable.merged?
+ :merged
+ else
+ issuable.open? ? :opened : :closed
+ end
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e3e7daa49c5..e8ac8788d9d 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -59,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
+ grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c8061fcdc59..b5acb80b720 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -8,7 +8,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project] do
+ link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 4b745a5b969..07ddc691d85 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -16,7 +16,7 @@ module TodosHelper
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo)
+ link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
end
def todo_target_path(todo)
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 3a83b083109..256cbcd73a1 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,10 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find(key_id)
+ @key = Key.find_by_id(key_id)
+
+ return unless @key
+
@current_user = @user = @key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1227458e525..7d33838044b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -37,8 +37,6 @@
module Ci
class Build < CommitStatus
- include Gitlab::Application.routes.url_helpers
-
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
@@ -128,7 +126,7 @@ module Ci
end
def retried?
- !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ !self.commit.latest_statuses_for_ref(self.ref).include?(self)
end
def depends_on_builds
@@ -309,22 +307,6 @@ module Ci
project.valid_runners_token? token
end
- def target_url
- namespace_project_build_url(project.namespace, project, self)
- end
-
- def cancel_url
- if active?
- cancel_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
- def retry_url
- if retryable?
- retry_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def can_be_served?(runner)
(tag_list - runner.tag_list).empty?
end
@@ -333,7 +315,7 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
- def show_warning?
+ def stuck?
pending? && !any_runners_online?
end
@@ -348,18 +330,6 @@ module Ci
artifacts_file.exists?
end
- def artifacts_download_url
- if artifacts?
- download_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
- def artifacts_browse_url
- if artifacts_metadata?
- browse_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
def artifacts_metadata?
artifacts? && artifacts_metadata.exists?
end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index ecbd2078b1d..f4cf7034b14 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
-
validates_presence_of :sha
validate :valid_commit_sha
@@ -42,16 +40,6 @@ module Ci
project.id
end
- def last_build
- builds.order(:id).last
- end
-
- def retry
- latest_builds.each do |build|
- Ci::Build.retry(build)
- end
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a
end
- def latest_builds
- @latest_builds ||= builds.latest.to_a
+ def latest_statuses_for_ref(ref)
+ latest_statuses.select { |status| status.ref == ref }
end
- def latest_builds_for_ref(ref)
- latest_builds.select { |build| build.ref == ref }
+ def matrix_builds(build = nil)
+ matrix_builds = builds.latest.ordered
+ matrix_builds = matrix_builds.similar(build) if build
+ matrix_builds.to_a
end
def retried
@@ -170,7 +160,7 @@ module Ci
end
def duration
- duration_array = latest_statuses.map(&:duration).compact
+ duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
@@ -183,16 +173,12 @@ module Ci
end
def coverage
- coverage_array = latest_builds.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
- def matrix_for_ref?(ref)
- latest_builds_for_ref(ref).size > 1
- end
-
def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
@@ -218,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
- def update_committed!
- update!(committed_at: DateTime.now)
- end
-
private
def save_yaml_error(error)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index e725a6d468c..90349a07594 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -23,7 +23,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
-
+
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
@@ -46,9 +46,23 @@ module Ci
acts_as_taggable
+ # Searches for runners matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # This method performs a *partial* match on tokens, thus a query for "a"
+ # will match any runner where the token contains the letter "a". As a result
+ # you should *not* use this method for non-admin purposes as otherwise users
+ # might be able to query a list of all runners.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def self.search(query)
- where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
- query: "%#{query.try(:downcase)}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7ef50836322..3b1aa0f5c80 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def cancel_url
- nil
- end
-
- def retry_url
- nil
- end
-
- def show_warning?
+ def stuck?
false
end
-
- def artifacts_download_url
- nil
- end
-
- def artifacts_browse_url
- nil
- end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 27b97944e38..3c42f582937 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -61,12 +61,29 @@ module Issuable
end
module ClassMethods
+ # Searches for records with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(title) like :query", query: "%#{query.downcase}%")
+ where(arel_table[:title].matches("%#{query}%"))
end
+ # Searches for records with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def full_search(query)
- where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
diff --git a/app/models/group.rb b/app/models/group.rb
index 76042b3e3fd..9919ca112dc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -23,6 +23,8 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :project_group_links, dependent: :destroy
+ has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -33,8 +35,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
+ # Searches for groups matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
+ table = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
diff --git a/app/models/key.rb b/app/models/key.rb
index 406a1257b5d..0282ad18139 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -16,6 +16,7 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include AfterCommitQueue
include Sortable
belongs_to :user
@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
end
def notify_user
- NotificationService.new.new_key(self)
+ run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c1e18bb3cc5..188325045e2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
- scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
@@ -161,6 +160,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
+ # Returns all the merge requests from an ActiveRecord:Relation.
+ #
+ # This method uses a UNION as it usually operates on the result of
+ # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
+ # using multiple sub-queries especially when combined with an OR statement.
+ # UNIONs on the other hand perform much better in these cases.
+ #
+ # relation - An ActiveRecord::Relation that returns a list of Projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.in_projects(relation)
+ source = where(source_project_id: relation).select(:id)
+ target = where(target_project_id: relation).select(:id)
+ union = Gitlab::SQL::Union.new([source, target])
+
+ where("merge_requests.id IN (#{union.to_sql})")
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e3969f32dd6..374590ba0c5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
+ Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
@@ -58,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
+ # Searches for milestones matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- query = "%#{query}%"
- where("title like ? or description like ?", query, query)
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
@@ -72,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/)
end
+ def self.upcoming
+ self.where('due_date > ?', Time.now).order(due_date: :asc).first
+ end
+
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index bdb33f37495..55842df1e2d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
+ # Searches for namespaces matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation
def search(query)
- where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
diff --git a/app/models/note.rb b/app/models/note.rb
index 3b20d5d22b6..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
+ before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
- scope :not_inline, ->{ where(line_code: [nil, '']) }
+ scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(note) like :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(table[:note].matches(pattern))
end
def grouped_awards
@@ -162,26 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end
- # Check if such line of code exists in merge request diff
- # If exists - its active discussion
- # If not - its outdated diff
+ # Check if this note is part of an "active" discussion
+ #
+ # This will always return true for anything except MergeRequest noteables,
+ # which have special logic.
+ #
+ # If the note's current diff cannot be matched in the MergeRequest's current
+ # diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
return @active if defined?(@active)
- diffs = noteable.diffs(Commit.max_diff_options)
- notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
+ noteable_diff = find_noteable_diff
- return @active = false if notable_diff.nil?
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
- parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
- # We cannot use ||= because @active may be false
- @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
- end
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
+ else
+ @active = false
+ end
- def outdated?
- !active?
+ @active
end
def diff_file_index
@@ -365,6 +379,16 @@ class Note < ActiveRecord::Base
private
+ def clear_blank_line_code!
+ self.line_code = nil if self.line_code.blank?
+ end
+
+ # Find the diff on noteable that matches our own
+ def find_noteable_diff
+ diffs = noteable.diffs(Commit.max_diff_options)
+ diffs.find { |d| d.new_path == self.diff.new_path }
+ end
+
def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 65829bec77a..89a55a510cd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :project_group_links, dependent: :destroy
+ has_many :invited_groups, through: :project_group_links, source: :group
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
@@ -266,13 +268,38 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
+ # Searches for a list of projects based on the query given in `query`.
+ #
+ # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
+ # search. On MySQL a regular "LIKE" is used as it's already
+ # case-insensitive.
+ #
+ # query - The search query as a String.
def search(query)
- joins(:namespace).
- where('LOWER(projects.name) LIKE :query OR
- LOWER(projects.path) LIKE :query OR
- LOWER(namespaces.name) LIKE :query OR
- LOWER(projects.description) LIKE :query',
- query: "%#{query.try(:downcase)}%")
+ ptable = arel_table
+ ntable = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ projects = select(:id).where(
+ ptable[:path].matches(pattern).
+ or(ptable[:name].matches(pattern)).
+ or(ptable[:description].matches(pattern))
+ )
+
+ # We explicitly remove any eager loading clauses as they're:
+ #
+ # 1. Not needed by this query
+ # 2. Combined with .joins(:namespace) lead to all columns from the
+ # projects & namespaces tables being selected, leading to a SQL error
+ # due to the columns of all UNION'd queries no longer being the same.
+ namespaces = select(:id).
+ except(:includes).
+ joins(:namespace).
+ where(ntable[:name].matches(pattern))
+
+ union = Gitlab::SQL::Union.new([projects, namespaces])
+
+ where("projects.id IN (#{union.to_sql})")
end
def search_by_visibility(level)
@@ -280,7 +307,10 @@ class Project < ActiveRecord::Base
end
def search_by_title(query)
- non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
+ pattern = "%#{query}%"
+ table = Project.arel_table
+
+ non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
@@ -485,6 +515,7 @@ class Project < ActiveRecord::Base
end
def external_issue_tracker
+ return @external_issue_tracker if defined?(@external_issue_tracker)
@external_issue_tracker ||=
services.issue_trackers.active.without_defaults.first
end
@@ -878,6 +909,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
+ def allowed_to_share_with_group?
+ !namespace.share_with_group_lock
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
@@ -909,13 +944,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
- self.runners_token && self.runners_token == token
+ self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
- self.builds_enabled? && self.runners_token && self.runners_token == token
+ self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+class ProjectGroupLink < ActiveRecord::Base
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MASTER = 40
+
+ belongs_to :project
+ belongs_to :group
+
+ validates :project_id, presence: true
+ validates :group_id, presence: true
+ validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
+ validates :group_access, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
+ validate :different_group
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ DEVELOPER
+ end
+
+ def human_access
+ self.class.access_options.key(self.group_access)
+ end
+
+ private
+
+ def different_group
+ if self.group && self.project && self.project.group == self.group
+ errors.add(:base, "Project cannot be shared with the project it is in.")
+ end
+ end
+end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index e10b5529b42..d9f0849d147 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci'
def valid_token?(token)
- self.respond_to?(:token) && self.token.present? && self.token == token
+ self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9629c7e1bb9..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -160,7 +160,27 @@ class ProjectTeam
end
end
- access.max
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ access << max_invited_level(user_id)
+ end
+
+ access.compact.max
+ end
+
+
+ def max_invited_level(user_id)
+ project.project_group_links.map do |group_link|
+ invited_group = group_link.group
+ access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+
+ # If group member has higher access level we should restrict it
+ # to max allowed access level
+ if access && access > group_link.group_access
+ access = group_link.group_access
+ end
+
+ access
+ end.compact.max
end
private
@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
+ invited_members = []
+
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_group = group_link.group
+ im = invited_group.group_members
+
+ if level
+ int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # Skip group members if we ask for masters
+ # but max group access is developers
+ next if int_level > group_link.group_access
+
+ # If we ask for developers and max
+ # group access is developers we need to provide
+ # both group master, developers as devs
+ if int_level == group_link.group_access
+ im.where("access_level >= ?)", group_link.group_access)
+ else
+ im.send(level)
+ end
+ end
+
+ invited_members << im
+ end
+
+ invited_members = invited_members.flatten.compact
+ end
if level
project_members = project_members.send(level)
@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
+ user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index dd3925c7a7d..b9e835a4486 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base
end
class << self
+ # Searches for snippets with a matching title or file name.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
+ # Searches for snippets with matching content.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search_code(query)
- where('(content LIKE :query)', query: "%#{query}%")
+ table = Snippet.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:content].matches(pattern))
end
def accessible_to(user)
diff --git a/app/models/user.rb b/app/models/user.rb
index 505a547d8ec..68b242888aa 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -98,9 +98,6 @@ class User < ActiveRecord::Base
# Virtual attribute for authenticating by either username or email
attr_accessor :login
- # Virtual attributes to define avatar cropping
- attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_size
-
#
# Relations
#
@@ -166,11 +163,6 @@ class User < ActiveRecord::Base
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size,
- numericality: { only_integer: true },
- presence: true,
- if: ->(user) { user.avatar? && user.avatar_changed? }
-
before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs
@@ -286,8 +278,22 @@ class User < ActiveRecord::Base
end
end
+ # Searches users matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern))
+ )
end
def by_login(login)
@@ -818,7 +824,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
- projects.select(:id)])
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 005a5c4661c..50c95ced8a7 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -3,7 +3,7 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.ordered.find_by(sha: sha)
+ commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name)
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 31b407efeb1..69d5c42a877 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci?
# Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.update_committed!
commit.create_builds(ref, tag, user)
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index bd31a617747..d840ab5e340 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -49,6 +49,8 @@ class GitPushService < BaseService
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
update_merge_requests
+
+ perform_housekeeping
end
def update_main_language
@@ -73,6 +75,13 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id)
end
+ def perform_housekeeping
+ housekeeping = Projects::HousekeepingService.new(@project)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Projects::HousekeepingService::LeaseTaken
+ end
+
def process_default_branch
@push_commits = project.repository.commits(params[:newrev])
@@ -80,7 +89,7 @@ class GitPushService < BaseService
project.change_head(branch_name)
# Set protection on the default branch if configured
- if (current_application_settings.default_branch_protection != PROTECTION_NONE)
+ if current_application_settings.default_branch_protection != PROTECTION_NONE
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 0db85ac2142..bccd67d3dbf 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -9,12 +9,39 @@ module Projects
class HousekeepingService < BaseService
include Gitlab::ShellAdapter
+ LEASE_TIMEOUT = 3600
+
+ class LeaseTaken < StandardError
+ def to_s
+ "Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes"
+ end
+ end
+
def initialize(project)
@project = project
end
def execute
+ raise LeaseTaken if !try_obtain_lease
+
GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ ensure
+ @project.update_column(:pushes_since_gc, 0)
+ end
+
+ def needed?
+ @project.pushes_since_gc >= 10
+ end
+
+ def increment!
+ @project.increment!(:pushes_since_gc)
+ end
+
+ private
+
+ def try_obtain_lease
+ lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+ lease.try_obtain
end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e904cb6c6fc..e1e94c5cc38 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- project_ids = projects.pluck(:id)
- Gitlab::SearchResults.new(project_ids, params[:search])
+ Gitlab::SearchResults.new(projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index f630c0a3790..c08881dce4b 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,7 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project.id,
+ Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 8ca0877321d..0b3e713e220 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,8 +7,9 @@ module Search
end
def execute
- snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
- Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
+ snippets = Snippet.accessible_to(current_user)
+
+ Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 2c72df44ff0..6135c3ad96f 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -2,22 +2,11 @@
class AvatarUploader < CarrierWave::Uploader::Base
include UploaderHelper
- include CarrierWave::MiniMagick
storage :file
after :store, :reset_events_cache
- process :cropper
-
- def cropper
- return unless model.respond_to?(:avatar_crop_size) && model.valid?
-
- manipulate! do |img|
- img.crop "#{model.avatar_crop_size}x#{model.avatar_crop_size}+#{model.avatar_crop_x}+#{model.avatar_crop_y}"
- end
- end
-
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 34d955568f2..588ad767426 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status)
%td.build-link
- - if can?(current_user, :read_build, project) && build.target_url
- = link_to build.target_url do
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
- - if build.show_warning?
+ - if build.stuck?
%i.fa.fa-warning.text-warning
%td
@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td
- = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
- = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
@@ -61,13 +61,12 @@
%td
.pull-right
- if can?(current_user, :read_build, project) && build.artifacts?
- = link_to build.artifacts_download_url, title: 'Download artifacts' do
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download
- if can?(current_user, :update_build, build.project)
- if build.active?
- - if build.cancel_url
- = link_to build.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retry_url
- = link_to build.retry_url, method: :post, title: 'Retry' do
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f7fd156b84a..264fa1bf0cd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -50,6 +50,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+ - if @group.shared_projects.any?
+ .panel.panel-default
+ .panel-heading
+ Projects shared with #{@group.name}
+ %span.badge
+ #{@group.shared_projects.count}
+ %ul.well-list
+ - @group.shared_projects.sort_by(&:name).each do |project|
+ %li
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ %span.label.label-gray
+ = repository_size(project)
+ %span.pull-right.light
+ %span.monospace= project.path_with_namespace + ".git"
+
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
deleted file mode 100644
index 11163813f3e..00000000000
--- a/app/views/ci/commits/_commit.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(commit.status)
- - if commit.running?
- &middot;
- = commit.stage
-
-
- %td.build-link
- = link_to ci_status_path(commit) do
- %strong #{commit.short_sha}
-
- %td.build-message
- %span= truncate_first_line(commit.git_commit_message)
-
- %td.build-branch
- - unless @ref
- %span
- - commit.refs.each do |ref|
- = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
-
- %td.duration
- - if commit.duration > 0
- #{time_interval_in_words commit.duration}
-
- %td.timestamp
- - if commit.finished_at
- %span #{time_ago_in_words commit.finished_at} ago
-
- - if commit.coverage
- %td.coverage
- #{commit.coverage}%
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 6a5c917049d..001a711b1dd 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,4 +1,10 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
+ - if defined? small
+ = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
+ %span.sr-only
+ Destroy
+ = icon('trash')
+ - else
+ = submit_tag 'Destroy', data: { confirm: "Are you sure?" }, class: submit_btn_css
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 98a61ab211b..906b0676150 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
- if application.errors.any?
.alert.alert-danger
%ul
@@ -6,25 +6,20 @@
%li= msg
.form-group
- = f.label :name, class: 'control-label'
-
- .col-sm-10
- = f.text_field :name, class: 'form-control', required: true
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: 'form-control', required: true
.form-group
- = f.label :redirect_uri, class: 'control-label'
-
- .col-sm-10
- = f.text_area :redirect_uri, class: 'form-control', required: true
+ = f.label :redirect_uri, class: 'label-light'
+ = f.text_area :redirect_uri, class: 'form-control', required: true
+ %span.help-block
+ Use one line per URI
+ - if Doorkeeper.configuration.native_redirect_uri
%span.help-block
- Use one line per URI
- - if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
- Use
- %code= Doorkeeper.configuration.native_redirect_uri
- for local tests
+ Use
+ %code= Doorkeeper.configuration.native_redirect_uri
+ for local tests
- .form-actions
- = f.submit 'Submit', class: "btn btn-create"
- = link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
+ .prepend-top-default
+ = f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ba4c5b86efb..ea0b66c932b 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,19 +1,83 @@
- page_title "Applications"
-%h3.page-title Your applications
-%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
+- header_title page_title, applications_profile_path
-.table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td= application.redirect_uri
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
- %td= render 'delete_form', application: application
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ - if user_oauth_applications?
+ Manage applications that can use GitLab as an OAuth provider,
+ and applications that you've authorized to use your account.
+ - else
+ Manage applications that you've authorized to use your account.
+ .col-lg-9
+ - if user_oauth_applications?
+ %h5.prepend-top-0
+ Add new application
+ = render 'form', application: @application
+ %hr
+ - if user_oauth_applications?
+ .oauth-applications
+ %h5
+ Your applications (#{@applications.size})
+ - if @applications.any?
+ .table-responsive
+ %table.table
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th.last-heading
+ %tbody
+ - @applications.each do |application|
+ %tr{id: "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td
+ = link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
+ %span.sr-only
+ Edit
+ = icon('pencil')
+ = render 'delete_form', application: application, small: true
+ - else
+ .profile-settings-message.text-center
+ You don't have any applications
+ .oauth-authorized-applications.prepend-top-20.append-bottom-default
+ - if user_oauth_applications?
+ %h5
+ Authorized applications (#{@authorized_tokens.size})
+
+ - if @authorized_tokens.any?
+ .table-responsive
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{id: "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %em Authorization was granted by entering your username and password in the application.
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', token: token
+ - else
+ .profile-settings-message.text-center
+ You don't have any authorized applications
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 4ba8b84fd92..dce4081288c 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
+ = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
&middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index abea86b026a..5753158c24d 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -3,7 +3,7 @@
.event-last-push
.event-last-push-text
%span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
%strong= event.ref_name
%span at
%strong= link_to_project event.project
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 8bed5cdb9cc..235bd46107e 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -5,7 +5,7 @@
%strong= event.ref_name
- else
%strong
- = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
at
= link_to_project event.project
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
new file mode 100644
index 00000000000..dc76599b776
--- /dev/null
+++ b/app/views/groups/_activities.html.haml
@@ -0,0 +1,12 @@
+.hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+.nav-block
+ - if current_user
+ .controls
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ %i.fa.fa-rss
+ = render 'shared/event_filter'
+
+.content_list
+= spinner
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 7cd8e9bea46..cca7dc27b1c 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,12 +1 @@
-.top-area
- .nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- - if @projects.present?
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- = render 'shared/projects/dropdown'
- - if can? current_user, :create_projects, @group
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
- = icon('plus')
- New Project
-
-= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..b1694c919d0
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1 @@
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
new file mode 100644
index 00000000000..f73e1d9e865
--- /dev/null
+++ b/app/views/groups/activity.html.haml
@@ -0,0 +1,9 @@
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+
+- page_title "Activity"
+- header_title group_title(@group, "Activity", activity_group_path(@group))
+
+%section.activities
+ = render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3430f56a9c9..83936d39b16 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -23,6 +23,15 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ .form-group
+ %hr
+ = f.label :share_with_group_lock, class: 'control-label' do
+ Share with group lock
+ .col-sm-10
+ .checkbox
+ = f.check_box :share_with_group_lock
+ %span.descr Prevent sharing a project with another group within this group
+
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 6148d8cb3d2..23a34ac36dd 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -27,31 +27,34 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-
- %ul.nav-links
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- %li
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
-
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
- .tab-content
- .tab-pane.active#activity
- .activity-filter-block
- - if current_user
- = render "events/event_last_push", event: @last_push
-
- = render 'shared/event_filter'
+ .top-area
+ %ul.nav-links
+ %li.active
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ All Projects
+ - if @shared_projects.present?
+ %li
+ = link_to "#shared", 'data-toggle' => 'tab' do
+ Shared Projects
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/dropdown'
+ - if can? current_user, :create_projects, @group
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
+ = icon('plus')
+ New Project
- .content_list{data: {href: events_group_path}}
- = spinner
-
- .tab-pane#projects
+ .tab-content
+ .tab-pane.active#projects
= render "projects", projects: @projects
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
+
- else
%p.nav-links.no-top
No projects to show
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index e5e2a59eaed..59411ae1da1 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
- = icon('dashboard fw')
+ = icon('group fw')
%span
Group
- if can?(current_user, :read_group, @group)
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ = icon('dashboard fw')
+ %span
+ Activity
- if current_user
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index f3ded04419b..3b9d31a6fc5 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,7 +17,7 @@
= icon('gear fw')
%span
Account
- = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
+ = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw')
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 319974e12c5..0ae83ee01eb 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 3359716202f..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -13,6 +13,12 @@
= icon('pencil-square-o fw')
%span
Project Settings
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ = icon('share-square-o fw')
+ %span
+ Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
deleted file mode 100644
index 86f35823406..00000000000
--- a/app/views/profiles/applications.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-- page_title "Applications"
-- header_title page_title, applications_profile_path
-
-.alert.alert-help.prepend-top-default
- - if user_oauth_applications?
- Manage applications that can use GitLab as an OAuth provider,
- and applications that you've authorized to use your account.
- - else
- Manage applications that you've authorized to use your account.
-
-- if user_oauth_applications?
- .oauth-applications
- %h3
- Your applications
- .pull-right
- = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- - if @applications.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
-
-.oauth-authorized-applications.prepend-top-20
- - if user_oauth_applications?
- %h3
- Authorized applications
-
- - if @authorized_tokens.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
- %tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- - else
- %p.light You don't have any authorized applications
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 3d1ba49491c..cd582ba7060 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,7 +1,4 @@
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
- = f.hidden_field :avatar_crop_x
- = f.hidden_field :avatar_crop_y
- = f.hidden_field :avatar_crop_size
-if @user.errors.any?
%div.alert.alert-danger
%ul
@@ -97,19 +94,3 @@
.prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
-
-.modal.modal-profile-crop
- .modal-dialog
- .modal-content
- .modal-header
- %button.close{type: 'button', data: {dismiss: 'modal'}}
- %span
- &times;
- %h4.modal-title
- Crop your new profile picture
- .modal-body
- %p
- %img.modal-profile-crop-image
- .modal-footer
- %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
- Set new profile picture
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 14f1d3226bb..811d304ea75 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -55,7 +55,6 @@
%th Coverage
%th
- - @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
+ = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index be7cc0f256c..b02aee3db21 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
+ - builds = @build.commit.matrix_builds(@build)
+ - if builds.size > 1
%ul.nav-links.no-top.no-bottom
- - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ - builds.each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status)
@@ -44,7 +45,7 @@
.pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- - if @build.show_warning?
+ - if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
%p
@@ -100,12 +101,12 @@
%h4.title Build artifacts
.center
.btn-group{ role: :group }
- = link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('download')
Download
- if @build.artifacts_metadata?
- = link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('folder-open')
Browse
@@ -115,10 +116,10 @@
- if can?(current_user, :update_build, @project)
.center
.btn-group{ role: :group }
- - if @build.cancel_url
- = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retry_url
- = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
- if @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..d22d1da8402
--- /dev/null
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -0,0 +1,76 @@
+%tr.build
+ %td.status
+ - if can?(current_user, :read_build, build)
+ = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
+ - else
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
+
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = build.stage
+
+ %td
+ = build.name
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if can?(current_user, :read_build, build) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if can?(current_user, :update_build, build)
+ - if build.active?
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index befad27666c..003b7c18d0e 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -43,8 +43,8 @@
%th Coverage
%th
- @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+ - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
+ = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any?
.gray-content-block.second-block
@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled?
%th Coverage
%th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
+ = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
deleted file mode 100644
index a3449d1ae05..00000000000
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%tr.commit_status
- %td.status
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
- = ci_icon_for_status(commit_status.status)
- = commit_status.status
- - else
- = ci_status_with_icon(commit_status.status)
-
- %td.commit_status-link
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url do
- %strong ##{commit_status.id}
- - else
- %strong ##{commit_status.id}
-
- - if commit_status.show_warning?
- %i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"}
-
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
-
- %td
- - if commit_status.ref
- = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- - else
- .light none
-
- - if defined?(runner) && runner
- %td
- - if commit_status.try(:runner)
- = runner_link(commit_status.runner)
- - else
- .light none
-
- - if defined?(stage) && stage
- %td
- = commit_status.stage
-
- %td
- = commit_status.name
-
- .pull-right
- - if commit_status.tags.any?
- - commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if commit_status.try(:trigger_request)
- %span.label.label-info triggered
- - if commit_status.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if commit_status.duration
- #{duration_in_words(commit_status.finished_at, commit_status.started_at)}
-
- %td.timestamp
- - if commit_status.finished_at
- %span #{time_ago_with_tooltip(commit_status.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if commit_status.try(:coverage)
- #{commit_status.coverage}%
-
- %td
- .pull-right
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url
- = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
- %i.fa.fa-download
- - if can?(current_user, :update_commit_status, commit_status)
- - if commit_status.active?
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
- = link_to commit_status.retry_url, method: :post, title: 'Retry' do
- %i.fa.fa-repeat
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..c15386b4883
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1,58 @@
+%tr.generic_commit_status
+ %td.status
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
+ - else
+ = ci_status_with_icon(generic_commit_status.status)
+
+ %td.generic_commit_status-link
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url do
+ %strong ##{generic_commit_status.id}
+ - else
+ %strong ##{generic_commit_status.id}
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if generic_commit_status.try(:runner)
+ = runner_link(generic_commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = generic_commit_status.stage
+
+ %td
+ = generic_commit_status.name
+
+ .pull-right
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ %td.duration
+ - if generic_commit_status.duration
+ #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+
+ %td.timestamp
+ - if generic_commit_status.finished_at
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml
deleted file mode 100644
index 87ac75a350f..00000000000
--- a/app/views/projects/go_import.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! 5
-%html
- %head
- - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/')
- %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"}
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- page_title "Groups"
+%h3.page_title Share project with other groups
+%p.light
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+%hr
+- if @group_links.present?
+ .enabled-groups.panel.panel-default
+ .panel-heading
+ Already shared with
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-right
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
+ %i.icon-remove
+ disable sharing
+ = link_to group do
+ %strong
+ %i.icon-folder-open
+ = group.name
+ %br
+ .light up to #{group_link.human_access}
+
+
+.available-groups
+ %h4
+ Can be shared with
+ %div
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
+ .form-group
+ = label_tag :link_group_id, 'Group', class: 'control-label'
+ .col-sm-10
+ = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ .form-group
+ = label_tag :link_group_access, 'Max access level', class: 'control-label'
+ .col-sm-10
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
+ .form-actions
+ = submit_tag "Share", class: "btn btn-create"
+
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @project_group_links.each do |group_links|
+ - shared_group = group_links.group
+ - shared_group_users_count = group_links.group.group_members.count
+ .panel.panel-default
+ .panel-heading
+ Shared with
+ %strong #{shared_group.name}
+ group, members with
+ %strong #{group_links.human_access}
+ role (#{shared_group_users_count})
+ - if current_user.can?(:admin_group, shared_group)
+ .panel-head-actions
+ = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.content-list
+ - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ - if shared_group_users_count > 20
+ %li
+ and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 0f8848a5cbe..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -18,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
+
+ - if @project_group_links.any? && @project.allowed_to_share_with_group?
+ = render "shared_group_members"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c3fbba2ba54..42a3c2c3f02 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -15,7 +15,7 @@
.filter-item.inline
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
@@ -94,7 +94,7 @@
%a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
diff --git a/config/application.rb b/config/application.rb
index d8d1e7b4679..2b103c4592d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -34,7 +34,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables)
+ config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url)
# Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index d82cfb3ec0c..31dceaebcad 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -203,11 +203,11 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
- # config.warden do |manager|
- # manager.failure_app = AnotherApp
- # manager.intercept_401 = false
- # manager.default_strategies(scope: :user).unshift :some_external_strategy
- # end
+ config.warden do |manager|
+ manager.failure_app = Gitlab::DeviseFailure
+ # manager.intercept_401 = false
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy
+ end
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb
new file mode 100644
index 00000000000..7e7896b4900
--- /dev/null
+++ b/config/initializers/go_get.rb
@@ -0,0 +1 @@
+Rails.application.config.middleware.use(Gitlab::Middleware::Go)
diff --git a/config/initializers/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb
new file mode 100644
index 00000000000..835f3ec5574
--- /dev/null
+++ b/config/initializers/mysql_ignore_postgresql_options.rb
@@ -0,0 +1,49 @@
+# This patches ActiveRecord so indexes created using the MySQL adapter ignore
+# any PostgreSQL specific options (e.g. `using: :gin`).
+#
+# These patches do the following for MySQL:
+#
+# 1. Indexes created using the :opclasses option are ignored (as they serve no
+# purpose on MySQL).
+# 2. When creating an index with `using: :gin` the `using` option is discarded
+# as :gin is not a valid value for MySQL.
+# 3. The `:opclasses` option is stripped from add_index_options in case it's
+# used anywhere other than in the add_index methods.
+
+if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
+ module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter < AbstractMysqlAdapter
+ alias_method :__gitlab_add_index, :add_index
+ alias_method :__gitlab_add_index_sql, :add_index_sql
+ alias_method :__gitlab_add_index_options, :add_index_options
+
+ def add_index(table_name, column_name, options = {})
+ unless options[:opclasses]
+ __gitlab_add_index(table_name, column_name, options)
+ end
+ end
+
+ def add_index_sql(table_name, column_name, options = {})
+ unless options[:opclasses]
+ __gitlab_add_index_sql(table_name, column_name, options)
+ end
+ end
+
+ def add_index_options(table_name, column_name, options = {})
+ if options[:using] and options[:using] == :gin
+ options = options.dup
+ options.delete(:using)
+ end
+
+ if options[:opclasses]
+ options = options.dup
+ options.delete(:opclasses)
+ end
+
+ __gitlab_add_index_options(table_name, column_name, options)
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
new file mode 100644
index 00000000000..820cc89ef57
--- /dev/null
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -0,0 +1,188 @@
+# rubocop:disable all
+
+# These changes add support for PostgreSQL operator classes when creating
+# indexes and dumping/loading schemas. Taken from Rails pull request
+# https://github.com/rails/rails/pull/19090.
+#
+# License:
+#
+# Copyright (c) 2004-2016 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'date'
+require 'set'
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+# As the Struct definition is changed in this PR/patch we have to first remove
+# the existing one.
+ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition)
+
+module ActiveRecord
+ module ConnectionAdapters #:nodoc:
+ # Abstract representation of an index definition on a table. Instances of
+ # this type are typically created and returned by methods in database
+ # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc:
+ end
+ end
+end
+
+
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ module SchemaStatements
+ def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ column_names = Array(column_name)
+ index_name = index_name(table_name, column: column_names)
+
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses)
+
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
+
+ using = "USING #{options[:using]}" if options[:using].present?
+
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
+ end
+
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
+ end
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns, index_options, algorithm, using]
+ end
+ end
+ end
+end
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module SchemaStatements
+ # Returns an array of indexes for the given table.
+ def indexes(table_name, name = nil)
+ result = query(<<-SQL, 'SCHEMA')
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND d.indisprimary = 'f'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ ORDER BY i.relname
+ SQL
+
+ result.map do |row|
+ index_name = row[0]
+ unique = row[1] == 't'
+ indkey = row[2].split(" ")
+ inddef = row[3]
+ oid = row[4]
+
+ columns = Hash[query(<<-SQL, "SCHEMA")]
+ SELECT a.attnum, a.attname
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
+
+ column_names = columns.values_at(*indkey).compact
+
+ unless column_names.empty?
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
+ opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
+ column, opclass = column_and_opclass.split(' ').map(&:strip)
+ [column, opclass] if opclass
+ end.compact]
+
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, opclasses)
+ end
+ end.compact
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}"
+ end
+
+ protected
+
+ def quoted_columns_for_index(column_names, options = {})
+ column_opclasses = options[:opclasses] || {}
+ column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"}
+ end
+ end
+ end
+ end
+end
+
+module ActiveRecord
+ class SchemaDumper
+ private
+
+ def indexes(table, stream)
+ if (indexes = @connection.indexes(table)).any?
+ add_index_statements = indexes.map do |index|
+ statement_parts = [
+ "add_index #{remove_prefix_and_suffix(index.table).inspect}",
+ index.columns.inspect,
+ "name: #{index.name.inspect}",
+ ]
+ statement_parts << 'unique: true' if index.unique
+
+ index_lengths = (index.lengths || []).compact
+ statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
+
+ index_orders = index.orders || {}
+ statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
+ statement_parts << "where: #{index.where.inspect}" if index.where
+ statement_parts << "using: #{index.using.inspect}" if index.using
+ statement_parts << "type: #{index.type.inspect}" if index.type
+ statement_parts << "opclasses: #{index.opclasses}" if index.opclasses.present?
+
+ " #{statement_parts.join(', ')}"
+ end
+
+ stream.puts add_index_statements.sort.join("\n")
+ stream.puts
+ end
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index a918b5bd3f0..780ad757c5b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -295,7 +295,7 @@ Rails.application.routes.draw do
resource :profile, only: [:show, :update] do
member do
get :audit_log
- get :applications
+ get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :update_username
@@ -382,7 +382,7 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :projects
- get :events
+ get :activity
end
scope module: :groups do
@@ -701,6 +701,8 @@ Rails.application.routes.draw do
end
end
+ resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
+
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
delete :delete_attachment
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
new file mode 100644
index 00000000000..395083f2a03
--- /dev/null
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -0,0 +1,10 @@
+class CreateProjectGroupLinks < ActiveRecord::Migration
+ def change
+ create_table :project_group_links do |t|
+ t.integer :project_id, null: false
+ t.integer :group_id, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
new file mode 100644
index 00000000000..00e3947a6bb
--- /dev/null
+++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb
@@ -0,0 +1,5 @@
+class AddAccessToProjectGroupLink < ActiveRecord::Migration
+ def change
+ add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
+ end
+end
diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb
new file mode 100644
index 00000000000..78d1a4538f2
--- /dev/null
+++ b/db/migrate/20150930110012_add_group_share_lock.rb
@@ -0,0 +1,5 @@
+class AddGroupShareLock < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :share_with_group_lock, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
new file mode 100644
index 00000000000..003169c13c6
--- /dev/null
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -0,0 +1,53 @@
+class AddTrigramIndexesForSearching < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ unless trigrams_enabled?
+ raise 'You must enable the pg_trgm extension. You can do so by running ' \
+ '"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \
+ 'done for every GitLab database. For more information see ' \
+ 'http://www.postgresql.org/docs/current/static/sql-createextension.html'
+ end
+
+ # trigram indexes are case-insensitive so we can just index the column
+ # instead of indexing lower(column)
+ to_index.each do |table, columns|
+ columns.each do |column|
+ execute "CREATE INDEX CONCURRENTLY index_#{table}_on_#{column}_trigram ON #{table} USING gin(#{column} gin_trgm_ops);"
+ end
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ to_index.each do |table, columns|
+ columns.each do |column|
+ remove_index table, name: "index_#{table}_on_#{column}_trigram"
+ end
+ end
+ end
+
+ def trigrams_enabled?
+ res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;")
+ row = res.first
+
+ row && row['enabled'] == 't' ? true : false
+ end
+
+ def to_index
+ {
+ ci_runners: [:token, :description],
+ issues: [:title, :description],
+ merge_requests: [:title, :description],
+ milestones: [:title, :description],
+ namespaces: [:name, :path],
+ notes: [:note],
+ projects: [:name, :path, :description],
+ snippets: [:title, :file_name],
+ users: [:username, :name, :email]
+ }
+ end
+end
diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
new file mode 100644
index 00000000000..49e787d9a9a
--- /dev/null
+++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
@@ -0,0 +1,9 @@
+class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
+ def up
+ execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
new file mode 100644
index 00000000000..5d30a38bc99
--- /dev/null
+++ b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
@@ -0,0 +1,5 @@
+class ProjectsAddPushesSinceGc < ActiveRecord::Migration
+ def change
+ add_column :projects, :pushes_since_gc, :integer, default: 0
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a74b86d8e2f..5027d2ba32f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,10 +11,11 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160309140734) do
+ActiveRecord::Schema.define(version: 20160314143402) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
@@ -258,6 +259,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "architecture"
end
+ add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
+
create_table "ci_services", force: :cascade do |t|
t.string "type"
t.string "title"
@@ -417,11 +421,13 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
+ add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
+ add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
t.integer "user_id"
@@ -543,12 +549,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
+ add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
+ add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
@@ -562,26 +570,31 @@ ActiveRecord::Schema.define(version: 20160309140734) do
end
add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
+ add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
+ add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
+ t.boolean "share_with_group_lock", default: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
+ add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
@@ -607,6 +620,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
+ add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
@@ -656,6 +670,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "project_group_links", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "group_access", default: 30, null: false
+ end
+
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
@@ -698,6 +720,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false
t.string "main_language"
+ t.integer "pushes_since_gc", default: 0
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -705,9 +728,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
+ add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
+ add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
@@ -749,9 +775,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "type"
t.string "title"
t.integer "project_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "active", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
t.text "properties"
t.boolean "template", default: false
t.boolean "push_events", default: true
@@ -785,7 +811,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
+ add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
+ add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
@@ -919,9 +947,12 @@ ActiveRecord::Schema.define(version: 20160309140734) do
add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
+ add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["name"], name: "index_users_on_name", using: :btree
+ add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
+ add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
diff --git a/doc/api/builds.md b/doc/api/builds.md
index d3ce72e59fc..4c0a47d1ea0 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -33,7 +33,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.802Z",
- "download_url": null,
"artifacts_file": {
"filename": "artifacts.zip",
"size": 1000
@@ -75,7 +74,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.727Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
@@ -139,7 +137,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
@@ -164,7 +161,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.957Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:33.913Z",
"id": 9,
@@ -226,7 +222,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.880Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z",
"id": 8,
@@ -315,7 +310,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
@@ -362,7 +356,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": null,
"id": 69,
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d4f0bafa2..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -145,6 +145,7 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
+ "expires_at": null,
"updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z"
}
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index fb802102e3a..a7acf37b5bc 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -51,6 +51,7 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
+ "expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z"
}
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 9e9486cd87a..3703f4b327a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -619,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid
Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure.
+### Share project with group
+
+Allow to share project with group.
+
+```
+POST /projects/:id/share
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `group_id` (required) - The ID of a group
+- `group_access` (required) - Level of permissions for sharing
+
## Hooks
Also called Project Hooks and Webhooks.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 2120b5b2850..4abc45bf9bb 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -3,6 +3,7 @@
### CI User documentation
- [Get started with GitLab CI](quick_start/README.md)
+- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your builds](runners/README.md)
@@ -14,24 +15,4 @@
- [Build artifacts](build_artifacts/README.md)
- [User permissions](permissions/README.md)
- [API](api/README.md)
-
-### CI Examples
-
-- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [Test your PHP applications](examples/php.md)
-- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
-- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
-- [Test Clojure applications](examples/test-clojure-application.md)
-- [Using `dpl` as deployment tool](deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
-
-### CI Services
-
-GitLab CI uses the `services` keyword to define what docker containers should
-be linked with your base image. Below is a list of examples you may use:
-
-- [Using MySQL](services/mysql.md)
-- [Using PostgreSQL](services/postgres.md)
-- [Using Redis](services/redis.md)
-- [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
+- [CI services (linked docker containers)](services/README.md)
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 9bd2f5aff22..c10f82054e2 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -64,7 +64,7 @@ Save the file and restart GitLab: `sudo service gitlab restart`.
For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
```
-gitlab-rails['gitlab_default_projects_features_builds'] = false
+gitlab_rails['gitlab_default_projects_features_builds'] = false
```
Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 31f29f4a082..cc059dc4376 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,13 +1,15 @@
-## Build script examples
+# CI Examples
+- [Testing a PHP application](php.md)
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
+- [Using `dpl` as deployment tool](deployment/README.md)
+- Help your favorite programming language and GitLab by sending a merge request
+ with a guide for that language.
-## Languages
+## Outside the documentation
-This is a list of languages you can test with GitLab CI. Each section has
-comprehensive documentation and comes with a test repository hosted on
-GitLab.com.
-
-- [Testing PHP](php.md)
+- [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index c1bb47e4291..f5645d586ae 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,5 +1,5 @@
## Test and Deploy a ruby application
-This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
+This example will guide you how to run tests in your Ruby application and deploy it automatically as Heroku application.
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 624d9899c79..9aba4326e11 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -223,20 +223,13 @@ You can access a builds badge image using following link:
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
+Awesome! You started using CI in GitLab!
+
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
-## Next steps
-
-Awesome! You started using CI in GitLab!
-
-Next you can look into doing more with the CI. Many people are using GitLab
-to package, containerize, test and deploy software.
-
-Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>.
-
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 051eaa04152..5158e3c387c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -116,7 +116,8 @@ Alias for [stages](#stages).
### variables
-_**Note:** Introduced in GitLab Runner v0.5.0._
+>**Note:**
+Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
environment. The variables are stored in the git repository and are meant to
@@ -153,7 +154,8 @@ cache:
#### cache:key
-_**Note:** Introduced in GitLab Runner v1.0.0._
+>**Note:**
+Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs,
@@ -234,13 +236,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
-| stage | no (default: `test`) | Defines a build stage |
+| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -393,15 +396,18 @@ The above script will:
### artifacts
-_**Note:** Introduced in GitLab Runner v0.7.0 for non-Windows platforms._
-
-_**Note:** Limited Windows support was added in GitLab Runner v.1.0.0.
-Currently not all executors are supported._
-
-_**Note:** Build artifacts are only collected for successful builds._
+>**Notes:**
+>
+> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
+> - Windows support was added in GitLab Runner v.1.0.0.
+> - Currently not all executors are supported.
+> - Build artifacts are only collected for successful builds.
`artifacts` is used to specify list of files and directories which should be
-attached to build after success. Below are some examples.
+attached to build after success. To pass artifacts between different builds,
+see [dependencies](#dependencies).
+
+Below are some examples.
Send all files in `binaries` and `.config`:
@@ -453,9 +459,130 @@ release-job:
The artifacts will be sent to GitLab after a successful build and will
be available for download in the GitLab UI.
+#### artifacts:name
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+
+The `name` directive allows you to define the name of the created artifacts
+archive. That way, you can have a unique name of every archive which could be
+useful when you'd like to download the archive from GitLab. The `artifacts:name`
+variable can make use of any of the [predefined variables](../variables/README.md).
+
+---
+
+**Example configurations**
+
+To create an archive with a name of the current build:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_BUILD_NAME"
+```
+
+To create an archive with a name of the current branch or tag including only
+the files that are untracked by Git:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+To create an archive with a name of the current build and the current branch or
+tag including only the files that are untracked by Git:
+
+```yaml
+job:
+ artifacts:
+ name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
+ untracked: true
+```
+
+To create an archive with a name of the current [stage](#stages) and branch name:
+
+```yaml
+job:
+ artifacts:
+ name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
+ untracked: true
+```
+
+---
+
+If you use **Windows Batch** to run your shell scripts you need to replace
+`$` with `%`:
+
+```yaml
+job:
+ artifacts:
+ name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+ untracked: true
+```
+
+### dependencies
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+This feature should be used in conjunction with [`artifacts`](#artifacts) and
+allows you to define the artifacts to pass between different builds.
+
+Note that `artifacts` from previous [stages](#stages) are passed by default.
+
+To use this feature, define `dependencies` in context of the job and pass
+a list of all previous builds from which the artifacts should be downloaded.
+You can only define builds from stages that are executed before the current one.
+An error will be shown if you define builds from the current stage or next ones.
+
+---
+
+In the following example, we define two jobs with artifacts, `build:osx` and
+`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
+will be downloaded and extracted in the context of the build. The same happens
+for `test:linux` and artifacts from `build:linux`.
+
+The job `deploy` will download artifacts from all previous builds because of
+the [stage](#stages) precedence:
+
+```yaml
+build:osx:
+ stage: build
+ script: make build:osx
+ artifacts:
+ paths:
+ - binaries/
+
+build:linux:
+ stage: build
+ script: make build:linux
+ artifacts:
+ paths:
+ - binaries/
+
+test:osx:
+ stage: test
+ script: make test:osx
+ dependencies:
+ - build:osx
+
+test:linux:
+ stage: test
+ script: make test:linux
+ dependencies:
+ - build:linux
+
+deploy:
+ stage: deploy
+ script: make deploy
+```
+
### cache
-_**Note:** Introduced in GitLab Runner v0.7.0._
+>**Note:**
+Introduced in GitLab Runner v0.7.0.
`cache` is used to specify list of files and directories which should be cached
between builds. Below are some examples:
@@ -509,6 +636,155 @@ rspec:
The cache is provided on best effort basis, so don't expect that cache will be
always present. For implementation details please check GitLab Runner.
+## Hidden jobs
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+use this feature to ignore jobs, or use the
+[special YAML features](#special-yaml-features) and transform the hidden jobs
+into templates.
+
+In the following example, `.job_name` will be ignored:
+
+```yaml
+.job_name:
+ script:
+ - rake spec
+```
+
+## Special YAML features
+
+It's possible to use special YAML features like anchors (`&`), aliases (`*`)
+and map merging (`<<`), which will allow you to greatly reduce the complexity
+of `.gitlab-ci.yml`.
+
+Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
+
+### Anchors
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+YAML also has a handy feature called 'anchors', which let you easily duplicate
+content across your document. Anchors can be used to duplicate/inherit
+properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+to provide templates for your jobs.
+
+The following example uses anchors and map merging. It will create two jobs,
+`test1` and `test2`, that will inherit the parameters of `.job_template`, each
+having their own custom `script` defined:
+
+```yaml
+.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test1 project
+
+test2:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test2 project
+```
+
+`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
+given hash into the current one", and `*` includes the named anchor
+(`job_definition` again). The expanded version looks like this:
+
+```yaml
+.job_template:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test1 project
+
+test2:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test2 project
+```
+
+Let's see another one example. This time we will use anchors to define two sets
+of services. This will create two jobs, `test:postgres` and `test:mysql`, that
+will share the `script` directive defined in `.job_template`, and the `services`
+directive defined in `.postgres_services` and `.mysql_services` respectively:
+
+```yaml
+.job_template: &job_definition
+ script:
+ - test project
+
+.postgres_services:
+ services: &postgres_definition
+ - postgres
+ - ruby
+
+.mysql_services:
+ services: &mysql_definition
+ - mysql
+ - ruby
+
+test:postgres:
+ << *job_definition
+ services: *postgres_definition
+
+test:mysql:
+ << *job_definition
+ services: *mysql_definition
+```
+
+The expanded version looks like this:
+
+```yaml
+.job_template:
+ script:
+ - test project
+
+.postgres_services:
+ services:
+ - postgres
+ - ruby
+
+.mysql_services:
+ services:
+ - mysql
+ - ruby
+
+test:postgres:
+ script:
+ - test project
+ services:
+ - postgres
+ - ruby
+
+test:mysql:
+ script:
+ - test project
+ services:
+ - mysql
+ - ruby
+```
+
+You can see that the hidden jobs are conveniently used as templates.
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 0fd54be58b0..4f011397269 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -233,9 +233,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-5-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
-**Note:** You can change `8-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 8df142c531b..d59b7f0e84d 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -97,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the
If you want to run the database separately expect a size of about 1 MB per user.
+### PostgreSQL Requirements
+
+Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
+GitLab database. This extension can be enabled (using a PostgreSQL super user)
+by running the following query for every database:
+
+ CREATE EXTENSION pg_trgm;
+
+On some systems you may need to install an additional package (e.g.
+`postgresql-contrib`) for this extension to become available.
+
## Redis and Sidekiq
Redis stores all user sessions and the background task queue.
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
new file mode 100644
index 00000000000..024f6e8a433
--- /dev/null
+++ b/doc/update/8.5-to-8.6.md
@@ -0,0 +1,164 @@
+# From 8.5 to 8.6
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.11
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout 0.6.5
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab-ssl origin/8-6-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab origin/8-6-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-6-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+### 8. Updates for PostgreSQL Users
+
+Starting with 8.6 users using GitLab in combination with PostgreSQL are required
+to have the `pg_trgm` extension enabled for all GitLab databases. If you're
+using GitLab's Omnibus packages there's nothing you'll need to do manually as
+this extension is enabled automatically. Users who install GitLab without using
+Omnibus (e.g. by building from source) have to enable this extension manually.
+To enable this extension run the following SQL command as a PostgreSQL super
+user for _every_ GitLab database:
+
+```sql
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+```
+
+Certain operating systems might require the installation of extra packages for
+this extension to be available. For example, users using Ubuntu will have to
+install the `postgresql-contrib` package in order for this extension to be
+available.
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.4 to 8.5](8.4-to-8.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 87049427139..afdf1a682e2 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -582,6 +582,7 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb",
+ "expires_at": null,
"type": "ProjectSnippet",
"visibility_level": 0
}
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 2ac32373ce9..25893f948ea 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,6 +13,8 @@
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
+- [Sharing a project with a group](share_with_group.md)
+- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
new file mode 100644
index 00000000000..71106a8a5a0
--- /dev/null
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
new file mode 100644
index 00000000000..cbf2c3c1fdc
--- /dev/null
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
new file mode 100644
index 00000000000..a5dbc89fe90
--- /dev/null
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
new file mode 100644
index 00000000000..4c59f59c587
--- /dev/null
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -0,0 +1,30 @@
+# Share Projects with other Groups
+
+In GitLab Enterprise Edition you can share projects with other groups.
+This makes it possible to add a group of users to a project with a single action.
+
+## Groups as collections of users
+
+In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
+In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+
+## Sharing a project with a group of users
+
+The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
+But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the (Enterprise Edition only) group sharing feature can be of use.
+
+To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
+
+![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+
+Now you can add the 'Engineering' group with the maximum access level of your choice.
+After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
+
+!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
+
+## Maximum access level
+
+!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
+
+In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md
new file mode 100644
index 00000000000..3b7690973cb
--- /dev/null
+++ b/doc/workflow/share_with_group.md
@@ -0,0 +1,13 @@
+# Sharing a project with a group
+
+If you want to share a single project in a group with another group,
+you can do so easily. By setting the permission you can quickly
+give a select group of users access to a project in a restricted manner.
+
+In a project go to the project settings -> groups.
+
+Now you can select a group that you want to share this project with and with
+which maximum access level. Users in that group are able to access this project
+with their set group access level, up to the maximum level that you've set.
+
+![Share a project with a group](share_with_group.png)
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
new file mode 100644
index 00000000000..a0ca6f14552
--- /dev/null
+++ b/doc/workflow/share_with_group.png
Binary files differ
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index 2edb3964f70..ab7de7ac315 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -21,6 +21,11 @@ Feature: Admin Groups
When I select user "John Doe" from user list as "Reporter"
Then I should see "John Doe" in team list in every project as "Reporter"
+ Scenario: Shared projects
+ Given group has shared projects
+ When I visit group page
+ Then I should see project shared with group
+
@javascript
Scenario: Remove user from group
Given we have user "John Doe" in group
diff --git a/features/groups.feature b/features/groups.feature
index a60c3860b83..419a5d3963d 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -15,6 +15,10 @@ Feature: Groups
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
Then I should see group "Owned" projects list
+
+ @javascript
+ Scenario: I should see group "Owned" activity feed
+ When I visit group "Owned" activity page
And I should see projects activity feed
Scenario: I should see group "Owned" issues list
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 168d9d30b50..447dd92a458 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -76,8 +76,7 @@ Feature: Profile
Scenario: I can manage application
Given I visit profile applications page
- Then I click on new application button
- And I should see application form
+ Then I should see application form
Then I fill application form out and submit
And I see application
Then I click edit
diff --git a/features/project/group_links.feature b/features/project/group_links.feature
new file mode 100644
index 00000000000..2657c4487ad
--- /dev/null
+++ b/features/project/group_links.feature
@@ -0,0 +1,16 @@
+Feature: Project Group Links
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" is shared with group "Ops"
+ And project "Shop" is not shared with group "Market"
+ And I visit project group links page
+
+ Scenario: I should see list of groups
+ Then I should see project already shared with group "Ops"
+ Then I should see project is not shared with group "Market"
+
+ @javascript
+ Scenario: I share project with group
+ When I select group "Market" for share
+ Then I should see project is shared with group "Market"
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index f8d9fe1854d..74685d24a7d 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -46,11 +46,18 @@ Feature: Project Merge Requests
Then I should see "Feature NS-03" in merge requests
And I should see "Bug NS-04" in merge requests
- Scenario: I visit merge request page
+ Scenario: I visit an open merge request page
Given I click link "Bug NS-04"
Then I should see merge request "Bug NS-04"
And I should see "1 of 1" in the sidebar
+ Scenario: I visit a merged merge request page
+ Given project "Shop" have "Feature NS-05" merged merge request
+ And I click link "Merged"
+ And I click link "Feature NS-05"
+ Then I should see merge request "Feature NS-05"
+ And I should see "3 of 3" in the sidebar
+
Scenario: I close merge request page
Given I click link "Bug NS-04"
And I click link "Close"
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 6cc89a15a78..89a02706bd2 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -34,9 +34,10 @@ Feature: Project Network Graph
@javascript
Scenario: I should filter selected tag
When I switch ref to "v1.0.0"
+ Then page should have "v1.0.0" in title
Then page should have content not containing "v1.0.0"
When click "Show only selected branch" checkbox
- Then page should not have content not containing "v1.0.0"
+ Then page should only have content from "v1.0.0"
When click "Show only selected branch" checkbox
Then page should have content not containing "v1.0.0"
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 06fb45c8bde..5888662fc3f 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -39,3 +39,8 @@ Feature: Project Team Management
And I click link "Import team from another project"
And I submit "Website" project for import team
Then I should see "Mike" in team list as "Reporter"
+
+ Scenario: See all members of projects shared group
+ Given I share project with group "OpenSource"
+ And I visit project "Shop" team page
+ Then I should see "Opensource" group user listing
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 43fd91d0d4c..e1f1db2872f 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end
end
+ step 'group has shared projects' do
+ share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = current_group.id
+ share_link.save!
+ end
+
+ step 'I visit group page' do
+ visit admin_group_path(current_group)
+ end
+
+ step 'I should see project shared with group' do
+ expect(page).to have_content(shared_project.name_with_namespace)
+ expect(page).to have_content "Projects shared with"
+ end
+
step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john)
end
@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
@group ||= Group.first
end
+ def shared_project
+ @shared_project ||= create(:empty_project)
+ end
+
def user_john
@user_john ||= User.find_by(name: "John Doe")
end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index d723300f485..f4a56865532 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -36,22 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- execute_script('$("#assignee_id").val("")')
- execute_script('$(".js-user-search").first().click()')
- sleep 1
- execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
- sleep 1
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
- execute_script('$(".js-user-search").first().click()')
- sleep 1
- execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
- sleep 1
- execute_script('$(".js-user-search").eq(1).click()')
- sleep 1
- execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
- sleep 1
+ find('.js-author-search').click
+ find('.dropdown-menu-user-full-name', match: :first).click
+
+ find('.js-assignee-search').click
+ find('.dropdown-menu-user-full-name', match: :first).click
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 7fc0e444e86..a2adc87f8ef 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- execute_script('$("#assignee_id").val("")')
- execute_script('$(".js-user-search").first().click()')
- sleep 0.5
- execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
- sleep 2
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
- execute_script('$(".js-user-search").first().click()')
- sleep 0.5
- execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
- sleep 2
- execute_script('$(".js-user-search").eq(1).click()')
- sleep 0.5
- execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
- sleep 2
+ find(".js-author-search").click
+ find(".dropdown-menu-author li a", match: :first).click
+ find(".js-assignee-search").click
+ find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(merge_request)
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index d9436e9e21a..909de31a479 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -27,7 +27,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I change my avatar' do
- attach_avatar
+ attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
+ click_button "Update profile settings"
+ @user.reload
end
step 'I should see new avatar' do
@@ -40,7 +42,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I have an avatar' do
- attach_avatar
+ attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
+ click_button "Update profile settings"
+ @user.reload
end
step 'I remove my avatar' do
@@ -180,18 +184,14 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
end
- step 'I click on new application button' do
- click_on 'New Application'
- end
-
step 'I should see application form' do
- expect(page).to have_content "New Application"
+ expect(page).to have_content "Add new application"
end
step 'I fill application form out and submit' do
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
- click_on "Submit"
+ click_on "Save application"
end
step 'I see application' do
@@ -211,7 +211,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I change name of application and submit' do
expect(page).to have_content "Edit application"
fill_in :doorkeeper_application_name, with: 'test_changed'
- click_on "Submit"
+ click_on "Save application"
end
step 'I see that application was changed' do
@@ -229,16 +229,4 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step "I see that application is removed" do
expect(page.find(".oauth-applications")).not_to have_content "test_changed"
end
-
- def attach_avatar
- attach_file :user_avatar, Rails.root.join(*%w(spec fixtures banana_sample.gif))
-
- page.find('#user_avatar_crop_x', visible: false).set('0')
- page.find('#user_avatar_crop_y', visible: false).set('0')
- page.find('#user_avatar_crop_size', visible: false).set('256')
-
- click_button "Update profile settings"
-
- @user.reload
- end
end
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 135e1d016ae..ce2554bc80d 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -13,7 +13,6 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
- sleep 0.3
end
end
@@ -46,12 +45,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I have award added' do
- sleep 0.2
-
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
- expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index df4259b9ddf..91fe19dd477 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -16,10 +16,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_link "Bug NS-04"
end
+ step 'I click link "Feature NS-05"' do
+ click_link "Feature NS-05"
+ end
+
step 'I click link "All"' do
click_link "All"
end
+ step 'I click link "Merged"' do
+ click_link "Merged"
+ end
+
step 'I click link "Closed"' do
click_link "Closed"
end
@@ -40,6 +48,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content "Bug NS-04"
end
+ step 'I should see merge request "Feature NS-05"' do
+ expect(page).to have_content "Feature NS-05"
+ end
+
step 'I should not see "master" branch' do
expect(find('.merge-request-info')).not_to have_content "master"
end
@@ -120,6 +132,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'project "Shop" have "Feature NS-05" merged merge request' do
+ create(:merged_merge_request,
+ title: "Feature NS-05",
+ source_project: project,
+ target_project: project,
+ author: project.users.first)
+ end
+
step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
create(:merge_request, :rebased,
title: "Bug NS-07",
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 7a83d32a240..9b59b682676 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
When 'I switch ref to "feature"' do
select 'feature', from: 'ref'
- sleep 2
end
When 'I switch ref to "v1.0.0"' do
select 'v1.0.0', from: 'ref'
- sleep 2
end
When 'click "Show only selected branch" checkbox' do
find('#filter_ref').click
- sleep 2
end
step 'page should have content not containing "v1.0.0"' do
@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
end
- step 'page should not have content not containing "v1.0.0"' do
+ step 'page should have "v1.0.0" in title' do
+ expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
+ end
+
+ step 'page should only have content from "v1.0.0"' do
page.within '.network-graph' do
expect(page).not_to have_content 'Change some files'
end
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
new file mode 100644
index 00000000000..739a85e5fa4
--- /dev/null
+++ b/features/steps/project/project_group_links.rb
@@ -0,0 +1,50 @@
+class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include Select2Helper
+
+ step 'I should see project already shared with group "Ops"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Ops"
+ end
+ end
+
+ step 'I should see project is not shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).not_to have_content "Market"
+ end
+ end
+
+ step 'I select group "Market" for share' do
+ group = Group.find_by(path: 'market')
+ select2(group.id, from: "#link_group_id")
+ select "Master", from: 'link_group_access'
+ click_button "Share"
+ end
+
+ step 'I should see project is shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Market"
+ end
+ end
+
+ step 'project "Shop" is shared with group "Ops"' do
+ group = create(:group, name: 'Ops')
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = group.id
+ share_link.save!
+ end
+
+ step 'project "Shop" is not shared with group "Market"' do
+ create(:group, name: 'Market', path: 'market')
+ end
+
+ step 'I visit project group links page' do
+ visit namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def project
+ @project ||= Project.find_by_name "Shop"
+ end
+end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index caad52def79..3fbcf770b62 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link('Remove user from team')
end
end
+
+ step 'I share project with group "OpenSource"' do
+ project = Project.find_by(name: 'Shop')
+ os_group = create(:group, name: 'OpenSource')
+ create(:project, group: os_group)
+ @os_user1 = create(:user)
+ @os_user2 = create(:user)
+ os_group.add_owner(@os_user1)
+ os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = os_group.id
+ share_link.save!
+ end
+
+ step 'I should see "Opensource" group user listing' do
+ expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
+ expect(page).to have_content(@os_user1.name)
+ expect(page).to have_content(@os_user2.name)
+ end
end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index f33ed7834fe..c4c7672a432 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -68,7 +68,7 @@ module SharedBuilds
end
step 'I see the build' do
- page.within('.commit_status') do
+ page.within('.build') do
expect(page).to have_content "##{@build.id}"
expect(page).to have_content @build.sha[0..7]
expect(page).to have_content @build.ref
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index e59bfbea998..b6d70a26c21 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -147,6 +147,10 @@ module SharedIssuable
expect_sidebar_content('2 of 2')
end
+ step 'I should see "3 of 3" in the sidebar' do
+ expect_sidebar_content('3 of 3')
+ end
+
step 'I click link "Next" in the sidebar' do
page.within '.issuable-sidebar' do
click_link 'Next'
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index da9d1503ebc..2bd8ea745e4 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -27,6 +27,10 @@ module SharedPaths
visit group_path(Group.find_by(name: "Owned"))
end
+ step 'I visit group "Owned" activity page' do
+ visit activity_group_path(Group.find_by(name: "Owned"))
+ end
+
step 'I visit group "Owned" issues page' do
visit issues_group_path(Group.find_by(name: "Owned"))
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5b5b8bd044b..9805e53624e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -144,6 +144,9 @@ module API
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
+
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ expose(:expires_at) { |snippet| nil }
end
class ProjectEntity < Grape::Entity
@@ -243,6 +246,10 @@ module API
end
end
+ class ProjectGroupLink < Grape::Entity
+ expose :id, :project_id, :group_id, :group_access
+ end
+
class Namespace < Grape::Entity
expose :id, :path, :kind
end
@@ -401,13 +408,6 @@ module API
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- # TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
- # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
- expose :download_url do |repo_obj, options|
- if options[:user_can_download_artifacts]
- repo_obj.artifacts_download_url
- end
- end
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit do |repo_obj, _options|
if repo_obj.respond_to?(:commit)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6067c8b4a5e..6fcb5261e40 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -290,6 +290,33 @@ module API
end
end
+ # Share project with group
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # group_id (required) - The ID of a group
+ # group_access (required) - Level of permissions for sharing
+ #
+ # Example Request:
+ # POST /projects/:id/share
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ required_attributes! [:group_id, :group_access]
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new
+ link.group_id = params[:group_id]
+ link.group_access = params[:group_access]
+ if link.save
+ present link, with: Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
# Upload a file
#
# Parameters:
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index abd79b329ae..e8011519608 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
- UNSAFE_PROTOCOLS = %w(javascript :javascript data vbscript).freeze
+ UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist
whitelist = super
@@ -64,7 +64,12 @@ module Banzai
return unless node.name == 'a'
return unless node.has_attribute?('href')
- if node['href'].start_with?(*UNSAFE_PROTOCOLS)
+ begin
+ uri = Addressable::URI.parse(node['href'])
+ uri.scheme.strip! if uri.scheme
+
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
node.remove_attribute('href')
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 28e074cd289..c89e1b51019 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -5,7 +5,9 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
+ :allow_failure, :type, :stage, :when, :artifacts, :cache,
+ :dependencies]
attr_reader :before_script, :image, :services, :variables, :path, :cache
@@ -60,6 +62,7 @@ module Ci
@jobs = {}
@config.each do |key, job|
+ next if key.to_s.start_with?('.')
stage = job[:stage] || job[:type] || DEFAULT_STAGE
@jobs[key] = { stage: stage }.merge(job)
end
@@ -81,6 +84,7 @@ module Ci
services: job[:services] || @services,
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
+ dependencies: job[:dependencies],
}.compact
}
end
@@ -143,6 +147,7 @@ module Ci
validate_job_stage!(name, job) if job[:stage]
validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts]
+ validate_job_dependencies!(name, job) if job[:dependencies]
end
private
@@ -216,6 +221,10 @@ module Ci
end
def validate_job_artifacts!(name, job)
+ if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
+ raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
+ end
+
if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
end
@@ -225,6 +234,22 @@ module Ci
end
end
+ def validate_job_dependencies!(name, job)
+ if !validate_array_of_strings(job[:dependencies])
+ raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
+ end
+
+ stage_index = stages.index(job[:stage])
+
+ job[:dependencies].each do |dependency|
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+
+ unless stages.index(@jobs[dependency][:stage]) < stage_index
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
+ end
+ end
+ end
+
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
new file mode 100644
index 00000000000..a78fde9d782
--- /dev/null
+++ b/lib/gitlab/devise_failure.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ class DeviseFailure < Devise::FailureApp
+ protected
+
+ # Override `Devise::FailureApp#request_format` to handle a special case
+ #
+ # This tells Devise to handle an unauthenticated `.zip` request as an HTML
+ # request (i.e., redirect to sign in).
+ #
+ # Otherwise, Devise would respond with a 401 Unauthorized with
+ # `Content-Type: application/zip` and a response body in plaintext, and the
+ # browser would freak out.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944
+ def request_format
+ if request.format == :zip
+ Mime::Type.lookup_by_extension(:html).ref
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index e2a85f29825..172c5441e36 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -45,10 +45,13 @@ module Gitlab
direction: :asc).each do |raw_data|
pull_request = PullRequestFormatter.new(project, raw_data)
- if !pull_request.cross_project? && pull_request.valid?
- merge_request = MergeRequest.create!(pull_request.attributes)
- import_comments(pull_request.number, merge_request)
- import_comments_on_diff(pull_request.number, merge_request)
+ if pull_request.valid?
+ merge_request = MergeRequest.new(pull_request.attributes)
+
+ if merge_request.save
+ import_comments(pull_request.number, merge_request)
+ import_comments_on_diff(pull_request.number, merge_request)
+ end
end
end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index f96fed0f5cf..4e507b090e8 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -17,16 +17,12 @@ module Gitlab
}
end
- def cross_project?
- source_repo.id != target_repo.id
- end
-
def number
raw_data.number
end
def valid?
- source_branch.present? && target_branch.present?
+ !cross_project? && source_branch.present? && target_branch.present?
end
private
@@ -53,6 +49,10 @@ module Gitlab
raw_data.body || ""
end
+ def cross_project?
+ source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
+ end
+
def description
formatter.author_line(author) + body
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
new file mode 100644
index 00000000000..50b0dd32380
--- /dev/null
+++ b/lib/gitlab/middleware/go.rb
@@ -0,0 +1,50 @@
+# A dumb middleware that returns a Go HTML document if the go-get=1 query string
+# is used irrespective if the namespace/project exists
+module Gitlab
+ module Middleware
+ class Go
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = Rack::Request.new(env)
+
+ if go_request?(request)
+ render_go_doc(request)
+ else
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def render_go_doc(request)
+ body = go_body(request)
+ response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
+ response.finish
+ end
+
+ def go_request?(request)
+ request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
+ end
+
+ def go_body(request)
+ base_url = Gitlab.config.gitlab.url
+ # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
+ # We can just ignore the paths and leave the namespace/project
+ path_info = request.env["PATH_INFO"]
+ path_info.sub!(/^\//, '')
+ project_path = path_info.split('/').first(2).join('/')
+ request_url = URI.join(base_url, project_path)
+ domain_path = strip_url(request_url.to_s)
+
+ "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n";
+ end
+
+ def strip_url(url)
+ url.gsub(/\Ahttps?:\/\//, '')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 70de6a74e76..0607a8b9592 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,8 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(project_id, query, repository_ref = nil)
- @project = Project.find(project_id)
+ def initialize(project, query, repository_ref = nil)
+ @project = project
@repository_ref = if repository_ref.present?
repository_ref
else
@@ -73,7 +73,7 @@ module Gitlab
end
def notes
- Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
+ project.notes.user.search(query).order('updated_at DESC')
end
def commits
@@ -84,8 +84,8 @@ module Gitlab
end
end
- def limit_project_ids
- [project.id]
+ def project_ids_relation
+ project
end
end
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 2ab2d4af797..f13528a2eea 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,12 +2,12 @@ module Gitlab
class SearchResults
attr_reader :query
- # Limit search results by passed project ids
+ # Limit search results by passed projects
# It allows us to search only for projects user has access to
- attr_reader :limit_project_ids
+ attr_reader :limit_projects
- def initialize(limit_project_ids, query)
- @limit_project_ids = limit_project_ids || Project.all
+ def initialize(limit_projects, query)
+ @limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
@@ -27,7 +27,8 @@ module Gitlab
end
def total_count
- @total_count ||= projects_count + issues_count + merge_requests_count + milestones_count
+ @total_count ||= projects_count + issues_count + merge_requests_count +
+ milestones_count
end
def projects_count
@@ -53,27 +54,29 @@ module Gitlab
private
def projects
- Project.where(id: limit_project_ids).search(query)
+ limit_projects.search(query)
end
def issues
- issues = Issue.where(project_id: limit_project_ids)
+ issues = Issue.where(project_id: project_ids_relation)
+
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
else
issues = issues.full_search(query)
end
+
issues.order('updated_at DESC')
end
def milestones
- milestones = Milestone.where(project_id: limit_project_ids)
+ milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.order('updated_at DESC')
end
def merge_requests
- merge_requests = MergeRequest.in_projects(limit_project_ids)
+ merge_requests = MergeRequest.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
@@ -89,5 +92,9 @@ module Gitlab
def per_page
20
end
+
+ def project_ids_relation
+ limit_projects.select(:id).reorder(nil)
+ end
end
end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index addda95be2b..e0e74ff8359 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -2,10 +2,10 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
- attr_reader :limit_snippet_ids
+ attr_reader :limit_snippets
- def initialize(limit_snippet_ids, query)
- @limit_snippet_ids = limit_snippet_ids
+ def initialize(limit_snippets, query)
+ @limit_snippets = limit_snippets
@query = query
end
@@ -35,11 +35,11 @@ module Gitlab
private
def snippet_titles
- Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
+ limit_snippets.search(query).order('updated_at DESC')
end
def snippet_blobs
- Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC')
+ limit_snippets.search_code(query).order('updated_at DESC')
end
def default_scope
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
index d4a380cc2ee..77436958711 100644
--- a/spec/controllers/namespaces_controller_spec.rb
+++ b/spec/controllers/namespaces_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe NamespacesController do
- let!(:user) { create(:user, :with_avatar) }
+ let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
describe "GET show" do
context "when the namespace belongs to a user" do
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index 85dff009bcf..ad5855df0a4 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Profiles::AvatarsController do
- let(:user) { create(:user, :with_avatar) }
+ let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) }
before do
sign_in(user)
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 0147bd2b953..2acbba469e3 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::ImportsController do
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'Started' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
expect(flash.now[:notice]).to eq 'Started'
end
@@ -45,7 +45,7 @@ describe Projects::ImportsController do
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { notice_now: 'In progress' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
expect(flash.now[:notice]).to eq 'In progress'
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 09ec4f18f9d..0ddbec9eac2 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -2,30 +2,41 @@ require "spec_helper"
describe Projects::RepositoriesController do
let(:project) { create(:project) }
- let(:user) { create(:user) }
describe "GET archive" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- it "uses Gitlab::Workhorse" do
- expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
+ context 'as a guest' do
+ it 'responds with redirect in correct format' do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ expect(response.content_type).to start_with 'text/html'
+ expect(response).to be_redirect
+ end
end
- context "when the service raises an error" do
+ context 'as a user' do
+ let(:user) { create(:user) }
before do
- allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
+ project.team << [user, :developer]
+ sign_in(user)
end
+ it "uses Gitlab::Workhorse" do
+ expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
- it "renders Not Found" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ end
+
+ context "when the service raises an error" do
+
+ before do
+ allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
+ end
+
+ it "renders Not Found" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
- expect(response.status).to eq(404)
+ expect(response.status).to eq(404)
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 6eee4dfe229..1893e946f5c 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -9,19 +9,6 @@ describe ProjectsController do
describe "GET show" do
- context "when requested by `go get`" do
- render_views
-
- it "renders the go-import meta tag" do
- get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project"
-
- expect(response.body).to include("name='go-import'")
-
- content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git"
- expect(response.body).to include("content='#{content}'")
- end
- end
-
context "rendering default project view" do
render_views
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 0d9f4b299bc..af5d043cf02 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe UploadsController do
- let!(:user) { create(:user, :with_avatar) }
+ let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
describe "GET show" do
context "when viewing a user avatar" do
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index ca1c636fce4..a9df5fa1d3a 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -56,6 +56,10 @@ FactoryGirl.define do
target_branch "feature"
end
+ trait :merged do
+ state :merged
+ end
+
trait :closed do
state :closed
end
@@ -84,6 +88,7 @@ FactoryGirl.define do
merge_user author
end
+ factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
new file mode 100644
index 00000000000..e73cc05f9d7
--- /dev/null
+++ b/spec/factories/project_group_links.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :project_group_link do
+ project
+ group
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 785c2a3d811..a5c60c51c5b 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -23,13 +23,6 @@ FactoryGirl.define do
end
end
- trait :with_avatar do
- avatar { fixture_file_upload(Rails.root.join(*%w(spec fixtures dk.png)), 'image/png') }
- avatar_crop_x 0
- avatar_crop_y 0
- avatar_crop_size 256
- end
-
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index f32641ef0f6..fae0da9d898 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -17,6 +17,10 @@ describe ProjectsFinder do
create(:project, :public, group: group, name: 'C', path: 'C')
end
+ let!(:shared_project) do
+ create(:project, :private, name: 'D', path: 'D')
+ end
+
let(:finder) { described_class.new }
describe 'without a group' do
@@ -56,7 +60,35 @@ describe ProjectsFinder do
describe 'with a user' do
subject { finder.execute(user, group: group) }
- it { is_expected.to eq([public_project, internal_project]) }
+ describe 'without shared projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+
+ describe 'with shared projects and group membership' do
+ before do
+ group.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
+
+ describe 'with shared projects and project membership' do
+ before do
+ shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
end
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8013b31524f..f6c1005d265 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -77,7 +77,7 @@ describe ApplicationHelper do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
it 'should return an url for the avatar' do
- user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -88,7 +88,7 @@ describe ApplicationHelper do
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
- user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -102,7 +102,7 @@ describe ApplicationHelper do
describe 'using a User' do
it 'should return an URL for the avatar' do
- user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 4a7b00c7660..27ce312b11c 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -149,10 +149,20 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
output: '<a href="java"></a>'
},
+ 'protocol-based JS injection: invalid URL char' => {
+ input: '<img src=java\script:alert("XSS")>',
+ output: '<img>'
+ },
+
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>'
},
+
+ 'protocol whitespace' => {
+ input: '<a href=" http://example.com/"></a>',
+ output: '<a href="http://example.com/"></a>'
+ }
}
protocols.each do |name, data|
@@ -177,6 +187,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(output.to_html).to eq '<a>XSS</a>'
end
+ it 'disallows invalid URIs' do
+ expect(Addressable::URI).to receive(:parse).with('foo://example.com').
+ and_raise(Addressable::URI::InvalidURIError)
+
+ input = '<a href="foo://example.com">Foo</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>Foo</a>'
+ end
+
it 'allows non-standard anchor schemes' do
exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
act = filter(exp)
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 1e98280d045..fab6412d29f 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -397,7 +397,7 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true },
+ artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
script: "rspec"
}
})
@@ -417,6 +417,7 @@ module Ci
image: "ruby:2.1",
services: ["mysql"],
artifacts: {
+ name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true
}
@@ -427,6 +428,73 @@ module Ci
end
end
+ describe "Dependencies" do
+ let(:config) do
+ {
+ build1: { stage: 'build', script: 'test' },
+ build2: { stage: 'build', script: 'test' },
+ test1: { stage: 'test', script: 'test', dependencies: dependencies },
+ test2: { stage: 'test', script: 'test' },
+ deploy: { stage: 'test', script: 'test' }
+ }
+ end
+
+ subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ context 'no dependencies' do
+ let(:dependencies) { }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'dependencies to builds' do
+ let(:dependencies) { [:build1, :build2] }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'undefined dependency' do
+ let(:dependencies) { [:undefined] }
+
+ it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
+ end
+
+ context 'dependencies to deploy' do
+ let(:dependencies) { [:deploy] }
+
+ it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ end
+ end
+
+ describe "Hidden jobs" do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { script: 'test' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ let(:config_processor) { GitlabCiYamlProcessor.new(config) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master") }
+
+ it "doesn't create jobs that starts with dot" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :normal_job,
+ only: nil,
+ commands: "\ntest",
+ tag_list: [],
+ options: {},
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
describe "YAML Alias/Anchor" do
it "is correctly supported for jobs" do
config = <<EOT
@@ -629,6 +697,13 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
+ it "returns errors if job artifacts:name is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
+ end
+
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
@@ -684,6 +759,13 @@ EOT
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
end
+
+ it "returns errors if job dependencies is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 6cebcb5009a..e49dcb42342 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -127,34 +127,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
- describe '#cross_project?' do
- context 'when source, and target repositories are the same' do
- let(:raw_data) { OpenStruct.new(base_data) }
-
- it 'returns false' do
- expect(pull_request.cross_project?).to eq false
- end
- end
-
- context 'when source repo is a fork' do
- let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
- let(:raw_data) { OpenStruct.new(base_data) }
-
- it 'returns true' do
- expect(pull_request.cross_project?).to eq true
- end
- end
-
- context 'when target repo is a fork' do
- let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
- let(:raw_data) { OpenStruct.new(base_data) }
-
- it 'returns true' do
- expect(pull_request.cross_project?).to eq true
- end
- end
- end
-
describe '#number' do
let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
@@ -166,24 +138,44 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
describe '#valid?' do
let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
- context 'when source and target branches exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
+ context 'when source, and target repositories are the same' do
+ context 'and source and target branches exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
- it 'returns true' do
- expect(pull_request.valid?).to eq true
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ context 'and source branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+
+ context 'and target branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
end
end
- context 'when source branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+ context 'when source repo is a fork' do
+ let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
+ let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
end
end
- context 'when target branch doesn not exists' do
- let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+ context 'when target repo is a fork' do
+ let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
+ let(:raw_data) { OpenStruct.new(base_data) }
it 'returns false' do
expect(pull_request.valid?).to eq false
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
new file mode 100644
index 00000000000..117a15264da
--- /dev/null
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::Go, lib: true do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ describe 'when go-get=0' do
+ it 'skips go-import generation' do
+ env = { 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=0' }
+ expect(app).to receive(:call).with(env).and_return('no-go')
+ middleware.call(env)
+ end
+ end
+
+ describe 'when go-get=1' do
+ it 'returns a document' do
+ env = { 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=1',
+ 'PATH_INFO' => '/group/project/path' }
+ resp = middleware.call(env)
+ expect(resp[0]).to eq(200)
+ expect(resp[1]['Content-Type']).to eq('text/html')
+ expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n"
+ expect(resp[2].body).to eq([expected_body])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index efc2e5f4ef1..09adbc07dcb 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') }
+ let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
@@ -14,7 +14,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) }
+ let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
new file mode 100644
index 00000000000..bb18f417858
--- /dev/null
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::SearchResults do
+ let!(:project) { create(:project, name: 'foo') }
+ let!(:issue) { create(:issue, project: project, title: 'foo') }
+
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, title: 'foo')
+ end
+
+ let!(:milestone) { create(:milestone, project: project, title: 'foo') }
+ let(:results) { described_class.new(Project.all, 'foo') }
+
+ describe '#total_count' do
+ it 'returns the total amount of search hits' do
+ expect(results.total_count).to eq(4)
+ end
+ end
+
+ describe '#projects_count' do
+ it 'returns the total amount of projects' do
+ expect(results.projects_count).to eq(1)
+ end
+ end
+
+ describe '#issues_count' do
+ it 'returns the total amount of issues' do
+ expect(results.issues_count).to eq(1)
+ end
+ end
+
+ describe '#merge_requests_count' do
+ it 'returns the total amount of merge requests' do
+ expect(results.merge_requests_count).to eq(1)
+ end
+ end
+
+ describe '#milestones_count' do
+ it 'returns the total amount of milestones' do
+ expect(results.milestones_count).to eq(1)
+ end
+ end
+
+ describe '#empty?' do
+ it 'returns true when there are no search results' do
+ allow(results).to receive(:total_count).and_return(0)
+
+ expect(results.empty?).to eq(true)
+ end
+
+ it 'returns false when there are search results' do
+ expect(results.empty?).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
new file mode 100644
index 00000000000..e86b9ef6a63
--- /dev/null
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::SnippetSearchResults do
+ let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
+
+ let(:results) { described_class.new(Snippet.all, 'foo') }
+
+ describe '#total_count' do
+ it 'returns the total amount of search hits' do
+ expect(results.total_count).to eq(2)
+ end
+ end
+
+ describe '#snippet_titles_count' do
+ it 'returns the amount of matched snippet titles' do
+ expect(results.snippet_titles_count).to eq(1)
+ end
+ end
+
+ describe '#snippet_blobs_count' do
+ it 'returns the amount of matched snippet blobs' do
+ expect(results.snippet_blobs_count).to eq(1)
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 5b575da34f3..c6758ccad39 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -11,7 +11,7 @@ describe Notify do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
let(:token) { 'kETLwRaayvigPq_x3SNM' }
-
+
subject { Notify.new_user_email(new_user.id, token) }
it_behaves_like 'an email sent from GitLab'
@@ -77,6 +77,10 @@ describe Notify do
it 'includes a link to ssh keys page' do
is_expected.to have_body_text /#{profile_keys_path}/
end
+
+ context 'with SSH key that does not exist' do
+ it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
+ end
end
describe 'user added email' do
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e3d3d453653..b7457808040 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -9,7 +9,7 @@ describe Ci::Build, models: true do
it { is_expected.to respond_to :trace_html }
- describe :first_pending do
+ describe '#first_pending' do
let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
before { first; second }
@@ -19,7 +19,7 @@ describe Ci::Build, models: true do
it('returns with the first pending build') { is_expected.to eq(first) }
end
- describe :create_from do
+ describe '#create_from' do
before do
build.status = 'success'
build.save
@@ -33,7 +33,7 @@ describe Ci::Build, models: true do
end
end
- describe :ignored? do
+ describe '#ignored?' do
subject { build.ignored? }
context 'if build is not allowed to fail' do
@@ -69,7 +69,7 @@ describe Ci::Build, models: true do
end
end
- describe :trace do
+ describe '#trace' do
subject { build.trace_html }
it { is_expected.to be_empty }
@@ -101,7 +101,7 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(commit.project.timeout) }
# end
- describe :options do
+ describe '#options' do
let(:options) do
{
image: "ruby:2.1",
@@ -122,25 +122,25 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(project.allow_git_fetch) }
# end
- describe :project do
+ describe '#project' do
subject { build.project }
it { is_expected.to eq(commit.project) }
end
- describe :project_id do
+ describe '#project_id' do
subject { build.project_id }
it { is_expected.to eq(commit.project_id) }
end
- describe :project_name do
+ describe '#project_name' do
subject { build.project_name }
it { is_expected.to eq(project.name) }
end
- describe :extract_coverage do
+ describe '#extract_coverage' do
context 'valid content & regex' do
subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
@@ -172,7 +172,7 @@ describe Ci::Build, models: true do
end
end
- describe :variables do
+ describe '#variables' do
context 'returns variables' do
subject { build.variables }
@@ -242,7 +242,7 @@ describe Ci::Build, models: true do
end
end
- describe :can_be_served? do
+ describe '#can_be_served?' do
let(:runner) { FactoryGirl.create :ci_runner }
before { build.project.runners << runner }
@@ -277,7 +277,7 @@ describe Ci::Build, models: true do
end
end
- describe :any_runners_online? do
+ describe '#any_runners_online?' do
subject { build.any_runners_online? }
context 'when no runners' do
@@ -312,8 +312,8 @@ describe Ci::Build, models: true do
end
end
- describe :show_warning? do
- subject { build.show_warning? }
+ describe '#stuck?' do
+ subject { build.stuck? }
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
@@ -343,35 +343,7 @@ describe Ci::Build, models: true do
end
end
- describe :artifacts_download_url do
- subject { build.artifacts_download_url }
-
- context 'artifacts file does not exist' do
- before { build.update_attributes(artifacts_file: nil) }
- it { is_expected.to be_nil }
- end
-
- context 'artifacts file exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to_not be_nil }
- end
- end
-
- describe :artifacts_browse_url do
- subject { build.artifacts_browse_url }
-
- it "should be nil if artifacts browser is unsupported" do
- allow(build).to receive(:artifacts_metadata?).and_return(false)
- is_expected.to be_nil
- end
-
- it 'should not be nil if artifacts browser is supported' do
- allow(build).to receive(:artifacts_metadata?).and_return(true)
- is_expected.to_not be_nil
- end
- end
-
- describe :artifacts? do
+ describe '#artifacts?' do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
@@ -386,7 +358,7 @@ describe Ci::Build, models: true do
end
- describe :artifacts_metadata? do
+ describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy }
@@ -398,7 +370,7 @@ describe Ci::Build, models: true do
end
end
- describe :repo_url do
+ describe '#repo_url' do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
@@ -412,7 +384,7 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
- describe :depends_on_builds do
+ describe '#depends_on_builds' do
let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' }
let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' }
let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' }
@@ -444,7 +416,7 @@ describe Ci::Build, models: true do
created_at: created_at)
end
- describe :merge_request do
+ describe '#merge_request' do
context 'when a MR has a reference to the commit' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request)
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index 4dc309a4255..412842337ba 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -32,50 +32,6 @@ describe Ci::Commit, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- describe :ordered do
- let(:project) { FactoryGirl.create :empty_project }
-
- it 'returns ordered list of commits' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit1])
- end
-
- it 'returns commits ordered by committed_at and id, with nulls last' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
- end
- end
-
- describe :last_build do
- subject { commit.last_build }
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it { is_expected.to be_a(Ci::Build) }
- it('returns with the most recently created build') { is_expected.to eq(@second) }
- end
-
- describe :retry do
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it "creates only a new build" do
- expect(commit.builds.count(:all)).to eq 2
- expect(commit.statuses.count(:all)).to eq 2
- commit.retry
- expect(commit.builds.count(:all)).to eq 3
- expect(commit.statuses.count(:all)).to eq 3
- end
- end
-
describe :valid_commit_sha do
context 'commit.sha can not start with 00000000' do
before do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index e891838672e..25e9e5eca48 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -132,4 +132,32 @@ describe Ci::Runner, models: true do
expect(runner.belongs_to_one_project?).to be_truthy
end
end
+
+ describe '#search' do
+ let(:runner) { create(:ci_runner, token: '123abc') }
+
+ it 'returns runners with a matching token' do
+ expect(described_class.search(runner.token)).to eq([runner])
+ end
+
+ it 'returns runners with a partially matching token' do
+ expect(described_class.search(runner.token[0..2])).to eq([runner])
+ end
+
+ it 'returns runners with a matching token regardless of the casing' do
+ expect(described_class.search(runner.token.upcase)).to eq([runner])
+ end
+
+ it 'returns runners with a matching description' do
+ expect(described_class.search(runner.description)).to eq([runner])
+ end
+
+ it 'returns runners with a partially matching description' do
+ expect(described_class.search(runner.description[0..2])).to eq([runner])
+ end
+
+ it 'returns runners with a matching description regardless of the casing' do
+ expect(described_class.search(runner.description.upcase)).to eq([runner])
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 600089802b2..aff384c2949 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -32,9 +32,54 @@ describe Issue, "Issuable" do
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
- it "matches by title" do
+ it 'returns notes with a matching title' do
+ expect(described_class.search(searchable_issue.title)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching title' do
expect(described_class.search('able')).to eq([searchable_issue])
end
+
+ it 'returns notes with a matching title regardless of the casing' do
+ expect(described_class.search(searchable_issue.title.upcase)).
+ to eq([searchable_issue])
+ end
+ end
+
+ describe ".full_search" do
+ let!(:searchable_issue) do
+ create(:issue, title: "Searchable issue", description: 'kittens')
+ end
+
+ it 'returns notes with a matching title' do
+ expect(described_class.full_search(searchable_issue.title)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching title' do
+ expect(described_class.full_search('able')).to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching title regardless of the casing' do
+ expect(described_class.full_search(searchable_issue.title.upcase)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching description' do
+ expect(described_class.full_search(searchable_issue.description)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching description' do
+ expect(described_class.full_search(searchable_issue.description)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching description regardless of the casing' do
+ expect(described_class.full_search(searchable_issue.description.upcase)).
+ to eq([searchable_issue])
+ end
end
describe "#today?" do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 3c995053eec..c9245fc9535 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -103,4 +103,30 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"])
end
end
+
+ describe '.search' do
+ it 'returns groups with a matching name' do
+ expect(described_class.search(group.name)).to eq([group])
+ end
+
+ it 'returns groups with a partially matching name' do
+ expect(described_class.search(group.name[0..2])).to eq([group])
+ end
+
+ it 'returns groups with a matching name regardless of the casing' do
+ expect(described_class.search(group.name.upcase)).to eq([group])
+ end
+
+ it 'returns groups with a matching path' do
+ expect(described_class.search(group.path)).to eq([group])
+ end
+
+ it 'returns groups with a partially matching path' do
+ expect(described_class.search(group.path[0..2])).to eq([group])
+ end
+
+ it 'returns groups with a matching path regardless of the casing' do
+ expect(described_class.search(group.path.upcase)).to eq([group])
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 59c40922abb..8bf68013fd2 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -80,6 +80,12 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:merge_when_build_succeeds) }
end
+ describe '.in_projects' do
+ it 'returns the merge requests for a set of projects' do
+ expect(described_class.in_projects(Project.all)).to eq([subject])
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 28f13100d15..de1757bf67a 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -181,4 +181,34 @@ describe Milestone, models: true do
expect(issue4.position).to eq(42)
end
end
+
+ describe '.search' do
+ let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
+
+ it 'returns milestones with a matching title' do
+ expect(described_class.search(milestone.title)).to eq([milestone])
+ end
+
+ it 'returns milestones with a partially matching title' do
+ expect(described_class.search(milestone.title[0..2])).to eq([milestone])
+ end
+
+ it 'returns milestones with a matching title regardless of the casing' do
+ expect(described_class.search(milestone.title.upcase)).to eq([milestone])
+ end
+
+ it 'returns milestones with a matching description' do
+ expect(described_class.search(milestone.description)).to eq([milestone])
+ end
+
+ it 'returns milestones with a partially matching description' do
+ expect(described_class.search(milestone.description[0..2])).
+ to eq([milestone])
+ end
+
+ it 'returns milestones with a matching description regardless of the casing' do
+ expect(described_class.search(milestone.description.upcase)).
+ to eq([milestone])
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index e0b3290e416..3c3a580942a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -41,13 +41,32 @@ describe Namespace, models: true do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
- describe :search do
- before do
- @namespace = create :namespace
+ describe '.search' do
+ let(:namespace) { create(:namespace) }
+
+ it 'returns namespaces with a matching name' do
+ expect(described_class.search(namespace.name)).to eq([namespace])
+ end
+
+ it 'returns namespaces with a partially matching name' do
+ expect(described_class.search(namespace.name[0..2])).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching name regardless of the casing' do
+ expect(described_class.search(namespace.name.upcase)).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching path' do
+ expect(described_class.search(namespace.path)).to eq([namespace])
end
- it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) }
- it { expect(Namespace.search('unknown')).to eq([]) }
+ it 'returns namespaces with a partially matching path' do
+ expect(described_class.search(namespace.path[0..2])).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching path regardless of the casing' do
+ expect(described_class.search(namespace.path.upcase)).to eq([namespace])
+ end
end
describe :move_dir do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 33085dac4ea..6b18936edb1 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -140,13 +140,19 @@ describe Note, models: true do
end
end
- describe :search do
- let!(:note) { create(:note, note: "WoW") }
+ describe '.search' do
+ let(:note) { create(:note, note: 'WoW') }
- it { expect(Note.search('wow')).to include(note) }
+ it 'returns notes with matching content' do
+ expect(described_class.search(note.note)).to eq([note])
+ end
+
+ it 'returns notes with matching content regardless of the casing' do
+ expect(described_class.search('WOW')).to eq([note])
+ end
end
- describe :grouped_awards do
+ describe '.grouped_awards' do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
@@ -163,6 +169,66 @@ describe Note, models: true do
end
end
+ describe '#active?' do
+ it 'is always true when the note has no associated diff' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(nil)
+
+ expect(note).to be_active
+ end
+
+ it 'is never true when the note has no noteable associated' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'returns the memoized value if defined' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(double)
+
+ note.instance_variable_set(:@active, 'foo')
+ expect(note).not_to receive(:find_noteable_diff)
+
+ expect(note.active?).to eq 'foo'
+ end
+
+ context 'for a merge request noteable' do
+ it 'is false when noteable has no matching diff' do
+ merge = build_stubbed(:merge_request, :simple)
+ note = build(:note, noteable: merge)
+
+ allow(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:find_noteable_diff).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'is true when noteable has a matching diff' do
+ merge = create(:merge_request, :simple)
+
+ # Generate a real line_code value so we know it will match. We use a
+ # random line from a random diff just for funsies.
+ diff = merge.diffs.to_a.sample
+ line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
+ code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+
+ # We're persisting in order to trigger the set_diff callback
+ note = create(:note, noteable: merge, line_code: code)
+
+ # Make sure we don't get a false positive from a guard clause
+ expect(note).to receive(:find_noteable_diff).and_call_original
+ expect(note).to be_active
+ end
+ end
+ end
+
describe "editable?" do
it "returns true" do
note = build(:note)
@@ -220,4 +286,12 @@ describe Note, models: true do
expect(note.is_award?).to be_falsy
end
end
+
+ describe 'clear_blank_line_code!' do
+ it 'clears a blank line code before validation' do
+ note = build(:note, line_code: ' ')
+
+ expect { note.valid? }.to change(note, :line_code).to(nil)
+ end
+ end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
new file mode 100644
index 00000000000..2fa6715fcaf
--- /dev/null
+++ b/spec/models/project_group_link_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProjectGroupLink do
+ describe "Associations" do
+ it { should belong_to(:group) }
+ it { should belong_to(:project) }
+ end
+
+ describe "Validation" do
+ let!(:project_group_link) { create(:project_group_link) }
+
+ it { should validate_presence_of(:project_id) }
+ it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
+ it { should validate_presence_of(:group_id) }
+ it { should validate_presence_of(:group_access) }
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2fa38a5d3d3..b8b9a455b83 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -582,7 +582,64 @@ describe Project, models: true do
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
end
+ end
+
+ describe '.search' do
+ let(:project) { create(:project, description: 'kitten mittens') }
+
+ it 'returns projects with a matching name' do
+ expect(described_class.search(project.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching name' do
+ expect(described_class.search(project.name[0..2])).to eq([project])
+ end
+
+ it 'returns projects with a matching name regardless of the casing' do
+ expect(described_class.search(project.name.upcase)).to eq([project])
+ end
+
+ it 'returns projects with a matching description' do
+ expect(described_class.search(project.description)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching description' do
+ expect(described_class.search('kitten')).to eq([project])
+ end
+
+ it 'returns projects with a matching description regardless of the casing' do
+ expect(described_class.search('KITTEN')).to eq([project])
+ end
+
+ it 'returns projects with a matching path' do
+ expect(described_class.search(project.path)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching path' do
+ expect(described_class.search(project.path[0..2])).to eq([project])
+ end
+ it 'returns projects with a matching path regardless of the casing' do
+ expect(described_class.search(project.path.upcase)).to eq([project])
+ end
+
+ it 'returns projects with a matching namespace name' do
+ expect(described_class.search(project.namespace.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching namespace name' do
+ expect(described_class.search(project.namespace.name[0..2])).to eq([project])
+ end
+
+ it 'returns projects with a matching namespace name regardless of the casing' do
+ expect(described_class.search(project.namespace.name.upcase)).to eq([project])
+ end
+
+ it 'returns projects when eager loading namespaces' do
+ relation = described_class.all.includes(:namespace)
+
+ expect(relation.search(project.namespace.name)).to eq([project])
+ end
end
describe '#rename_repo' do
@@ -647,4 +704,20 @@ describe Project, models: true do
project.expire_caches_before_rename('foo')
end
end
+
+ describe '.search_by_title' do
+ let(:project) { create(:project, name: 'kittens') }
+
+ it 'returns projects with a matching name' do
+ expect(described_class.search_by_title(project.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching name' do
+ expect(described_class.search_by_title('kitten')).to eq([project])
+ end
+
+ it 'returns projects with a matching name regardless of the casing' do
+ expect(described_class.search_by_title('KITTENS')).to eq([project])
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 7b63da005f0..bacb17a8883 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -67,6 +67,50 @@ describe ProjectTeam, models: true do
end
end
+ describe :max_invited_level do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
+ end
+
+ describe :max_member_access do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+
+ it "does not have an access" do
+ project.namespace.update(share_with_group_lock: true)
+ expect(project.team.max_member_access(master.id)).to be_nil
+ expect(project.team.max_member_access(reporter.id)).to be_nil
+ end
+ end
+
describe "#human_max_access" do
it 'returns Master role' do
user = create(:user)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 7e5b5499aea..5077ac7b62b 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -59,4 +59,48 @@ describe Snippet, models: true do
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
end
end
+
+ describe '.search' do
+ let(:snippet) { create(:snippet) }
+
+ it 'returns snippets with a matching title' do
+ expect(described_class.search(snippet.title)).to eq([snippet])
+ end
+
+ it 'returns snippets with a partially matching title' do
+ expect(described_class.search(snippet.title[0..2])).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching title regardless of the casing' do
+ expect(described_class.search(snippet.title.upcase)).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching file name' do
+ expect(described_class.search(snippet.file_name)).to eq([snippet])
+ end
+
+ it 'returns snippets with a partially matching file name' do
+ expect(described_class.search(snippet.file_name[0..2])).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching file name regardless of the casing' do
+ expect(described_class.search(snippet.file_name.upcase)).to eq([snippet])
+ end
+ end
+
+ describe '#search_code' do
+ let(:snippet) { create(:snippet, content: 'class Foo; end') }
+
+ it 'returns snippets with matching content' do
+ expect(described_class.search_code(snippet.content)).to eq([snippet])
+ end
+
+ it 'returns snippets with partially matching content' do
+ expect(described_class.search_code('class')).to eq([snippet])
+ end
+
+ it 'returns snippets with matching content regardless of the casing' do
+ expect(described_class.search_code('FOO')).to eq([snippet])
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 412101ac9f9..6290ab3ebec 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -174,32 +174,6 @@ describe User, models: true do
end
end
end
-
- describe 'avatar' do
- it 'only validates when avatar is present and changed' do
- user = build(:user, :with_avatar)
-
- user.avatar_crop_x = nil
- user.avatar_crop_y = nil
- user.avatar_crop_size = nil
-
- expect(user).not_to be_valid
- expect(user.errors.keys).
- to match_array %i(avatar_crop_x avatar_crop_y avatar_crop_size)
- end
-
- it 'does not validate when avatar has not changed' do
- user = create(:user, :with_avatar)
-
- expect { user.avatar_crop_x = nil }.not_to change(user, :valid?)
- end
-
- it 'does not validate when avatar is not present' do
- user = create(:user)
-
- expect { user.avatar_crop_y = nil }.not_to change(user, :valid?)
- end
- end
end
describe "Respond to" do
@@ -463,17 +437,43 @@ describe User, models: true do
end
end
- describe 'search' do
- let(:user1) { create(:user, username: 'James', email: 'james@testing.com') }
- let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }
+ describe '.search' do
+ let(:user) { create(:user) }
+
+ it 'returns users with a matching name' do
+ expect(described_class.search(user.name)).to eq([user])
+ end
+
+ it 'returns users with a partially matching name' do
+ expect(described_class.search(user.name[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching name regardless of the casing' do
+ expect(described_class.search(user.name.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching Email' do
+ expect(described_class.search(user.email)).to eq([user])
+ end
+
+ it 'returns users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching Email regardless of the casing' do
+ expect(described_class.search(user.email.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching username' do
+ expect(described_class.search(user.username)).to eq([user])
+ end
+
+ it 'returns users with a partially matching username' do
+ expect(described_class.search(user.username[0..2])).to eq([user])
+ end
- it "should be case insensitive" do
- expect(User.search(user1.username.upcase).to_a).to eq([user1])
- expect(User.search(user1.username.downcase).to_a).to eq([user1])
- expect(User.search(user2.username.upcase).to_a).to eq([user2])
- expect(User.search(user2.username.downcase).to_a).to eq([user2])
- expect(User.search(user1.username.downcase).to_a.size).to eq(2)
- expect(User.search(user2.username.downcase).to_a.size).to eq(1)
+ it 'returns users with a matching username regardless of the casing' do
+ expect(described_class.search(user.username.upcase)).to eq([user])
end
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
new file mode 100644
index 00000000000..3722ddf5a33
--- /dev/null
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9f2365a4832..a6699cdc81c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -747,6 +747,42 @@ describe API::API, api: true do
end
end
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "should share project with group" do
+ expect do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response.status).to eq 201
+ expect(json_response['group_id']).to eq group.id
+ expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+ end
+
+ it "should return a 400 error when group id is not given" do
+ post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when access level is not given" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 409 error when wrong params passed" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+ expect(response.status).to eq 409
+ expect(json_response['message']).to eq 'Group access is not included in the list'
+ end
+ end
+
describe 'GET /projects/search/:query' do
let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index a7e2e1b1792..145bc937560 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -401,6 +401,45 @@ describe GitPushService, services: true do
end
end
+ describe "housekeeping" do
+ let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+ before do
+ allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
+ end
+
+ it 'does not perform housekeeping when not needed' do
+ expect(housekeeping).not_to receive(:execute)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+
+ context 'when housekeeping is needed' do
+ before do
+ allow(housekeeping).to receive(:needed?).and_return(true)
+ end
+
+ it 'performs housekeeping' do
+ expect(housekeeping).to receive(:execute)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+
+ it 'does not raise an exception' do
+ allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+ end
+
+
+ it 'increments the push counter' do
+ expect(housekeeping).to receive(:increment!)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+ end
+
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
new file mode 100644
index 00000000000..93bf1b81fbe
--- /dev/null
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::HousekeepingService do
+ subject { Projects::HousekeepingService.new(project) }
+ let(:project) { create :project }
+
+ describe 'execute' do
+ before do
+ project.pushes_since_gc = 3
+ project.save!
+ end
+
+ it 'enqueues a sidekiq job' do
+ expect(subject).to receive(:try_obtain_lease).and_return(true)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
+
+ subject.execute
+ expect(project.pushes_since_gc).to eq(0)
+ end
+
+ it 'does not enqueue a job when no lease can be obtained' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ expect(GitlabShellWorker).not_to receive(:perform_async)
+
+ expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ expect(project.pushes_since_gc).to eq(0)
+ end
+ end
+
+ describe 'needed?' do
+ it 'when the count is low enough' do
+ expect(subject.needed?).to eq(false)
+ end
+
+ it 'when the count is high enough' do
+ allow(project).to receive(:pushes_since_gc).and_return(10)
+ expect(subject.needed?).to eq(true)
+ end
+ end
+
+ describe 'increment!' do
+ it 'increments the pushes_since_gc counter' do
+ expect(project.pushes_since_gc).to eq(0)
+ subject.increment!
+ expect(project.pushes_since_gc).to eq(1)
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/cropper.js b/vendor/assets/javascripts/cropper.js
deleted file mode 100755
index 84aa6119ec3..00000000000
--- a/vendor/assets/javascripts/cropper.js
+++ /dev/null
@@ -1,2972 +0,0 @@
-/*!
- * Cropper v2.2.5
- * https://github.com/fengyuanchen/cropper
- *
- * Copyright (c) 2014-2016 Fengyuan Chen and contributors
- * Released under the MIT license
- *
- * Date: 2016-01-18T05:42:50.800Z
- */
-
-(function (factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as anonymous module.
- define(['jquery'], factory);
- } else if (typeof exports === 'object') {
- // Node / CommonJS
- factory(require('jquery'));
- } else {
- // Browser globals.
- factory(jQuery);
- }
-})(function ($) {
-
- 'use strict';
-
- // Globals
- var $window = $(window);
- var $document = $(document);
- var location = window.location;
- var ArrayBuffer = window.ArrayBuffer;
- var Uint8Array = window.Uint8Array;
- var DataView = window.DataView;
- var btoa = window.btoa;
-
- // Constants
- var NAMESPACE = 'cropper';
-
- // Classes
- var CLASS_MODAL = 'cropper-modal';
- var CLASS_HIDE = 'cropper-hide';
- var CLASS_HIDDEN = 'cropper-hidden';
- var CLASS_INVISIBLE = 'cropper-invisible';
- var CLASS_MOVE = 'cropper-move';
- var CLASS_CROP = 'cropper-crop';
- var CLASS_DISABLED = 'cropper-disabled';
- var CLASS_BG = 'cropper-bg';
-
- // Events
- var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown';
- var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove';
- var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel';
- var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
- var EVENT_DBLCLICK = 'dblclick';
- var EVENT_LOAD = 'load.' + NAMESPACE;
- var EVENT_ERROR = 'error.' + NAMESPACE;
- var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace
- var EVENT_BUILD = 'build.' + NAMESPACE;
- var EVENT_BUILT = 'built.' + NAMESPACE;
- var EVENT_CROP_START = 'cropstart.' + NAMESPACE;
- var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE;
- var EVENT_CROP_END = 'cropend.' + NAMESPACE;
- var EVENT_CROP = 'crop.' + NAMESPACE;
- var EVENT_ZOOM = 'zoom.' + NAMESPACE;
-
- // RegExps
- var REGEXP_ACTIONS = /e|w|s|n|se|sw|ne|nw|all|crop|move|zoom/;
- var REGEXP_DATA_URL = /^data\:/;
- var REGEXP_DATA_URL_HEAD = /^data\:([^\;]+)\;base64,/;
- var REGEXP_DATA_URL_JPEG = /^data\:image\/jpeg.*;base64,/;
-
- // Data keys
- var DATA_PREVIEW = 'preview';
- var DATA_ACTION = 'action';
-
- // Actions
- var ACTION_EAST = 'e';
- var ACTION_WEST = 'w';
- var ACTION_SOUTH = 's';
- var ACTION_NORTH = 'n';
- var ACTION_SOUTH_EAST = 'se';
- var ACTION_SOUTH_WEST = 'sw';
- var ACTION_NORTH_EAST = 'ne';
- var ACTION_NORTH_WEST = 'nw';
- var ACTION_ALL = 'all';
- var ACTION_CROP = 'crop';
- var ACTION_MOVE = 'move';
- var ACTION_ZOOM = 'zoom';
- var ACTION_NONE = 'none';
-
- // Supports
- var SUPPORT_CANVAS = $.isFunction($('<canvas>')[0].getContext);
-
- // Maths
- var num = Number;
- var min = Math.min;
- var max = Math.max;
- var abs = Math.abs;
- var sin = Math.sin;
- var cos = Math.cos;
- var sqrt = Math.sqrt;
- var round = Math.round;
- var floor = Math.floor;
-
- // Utilities
- var fromCharCode = String.fromCharCode;
-
- function isNumber(n) {
- return typeof n === 'number' && !isNaN(n);
- }
-
- function isUndefined(n) {
- return typeof n === 'undefined';
- }
-
- function toArray(obj, offset) {
- var args = [];
-
- // This is necessary for IE8
- if (isNumber(offset)) {
- args.push(offset);
- }
-
- return args.slice.apply(obj, args);
- }
-
- // Custom proxy to avoid jQuery's guid
- function proxy(fn, context) {
- var args = toArray(arguments, 2);
-
- return function () {
- return fn.apply(context, args.concat(toArray(arguments)));
- };
- }
-
- function isCrossOriginURL(url) {
- var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);
-
- return parts && (
- parts[1] !== location.protocol ||
- parts[2] !== location.hostname ||
- parts[3] !== location.port
- );
- }
-
- function addTimestamp(url) {
- var timestamp = 'timestamp=' + (new Date()).getTime();
-
- return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
- }
-
- function getCrossOrigin(crossOrigin) {
- return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : '';
- }
-
- function getImageSize(image, callback) {
- var newImage;
-
- // Modern browsers
- if (image.naturalWidth) {
- return callback(image.naturalWidth, image.naturalHeight);
- }
-
- // IE8: Don't use `new Image()` here (#319)
- newImage = document.createElement('img');
-
- newImage.onload = function () {
- callback(this.width, this.height);
- };
-
- newImage.src = image.src;
- }
-
- function getTransform(options) {
- var transforms = [];
- var rotate = options.rotate;
- var scaleX = options.scaleX;
- var scaleY = options.scaleY;
-
- if (isNumber(rotate)) {
- transforms.push('rotate(' + rotate + 'deg)');
- }
-
- if (isNumber(scaleX) && isNumber(scaleY)) {
- transforms.push('scale(' + scaleX + ',' + scaleY + ')');
- }
-
- return transforms.length ? transforms.join(' ') : 'none';
- }
-
- function getRotatedSizes(data, isReversed) {
- var deg = abs(data.degree) % 180;
- var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
- var sinArc = sin(arc);
- var cosArc = cos(arc);
- var width = data.width;
- var height = data.height;
- var aspectRatio = data.aspectRatio;
- var newWidth;
- var newHeight;
-
- if (!isReversed) {
- newWidth = width * cosArc + height * sinArc;
- newHeight = width * sinArc + height * cosArc;
- } else {
- newWidth = width / (cosArc + sinArc / aspectRatio);
- newHeight = newWidth / aspectRatio;
- }
-
- return {
- width: newWidth,
- height: newHeight
- };
- }
-
- function getSourceCanvas(image, data) {
- var canvas = $('<canvas>')[0];
- var context = canvas.getContext('2d');
- var x = 0;
- var y = 0;
- var width = data.naturalWidth;
- var height = data.naturalHeight;
- var rotate = data.rotate;
- var scaleX = data.scaleX;
- var scaleY = data.scaleY;
- var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
- var rotatable = isNumber(rotate) && rotate !== 0;
- var advanced = rotatable || scalable;
- var canvasWidth = width;
- var canvasHeight = height;
- var translateX;
- var translateY;
- var rotated;
-
- if (scalable) {
- translateX = width / 2;
- translateY = height / 2;
- }
-
- if (rotatable) {
- rotated = getRotatedSizes({
- width: width,
- height: height,
- degree: rotate
- });
-
- canvasWidth = rotated.width;
- canvasHeight = rotated.height;
- translateX = rotated.width / 2;
- translateY = rotated.height / 2;
- }
-
- canvas.width = canvasWidth;
- canvas.height = canvasHeight;
-
- if (advanced) {
- x = -width / 2;
- y = -height / 2;
-
- context.save();
- context.translate(translateX, translateY);
- }
-
- if (rotatable) {
- context.rotate(rotate * Math.PI / 180);
- }
-
- // Should call `scale` after rotated
- if (scalable) {
- context.scale(scaleX, scaleY);
- }
-
- context.drawImage(image, floor(x), floor(y), floor(width), floor(height));
-
- if (advanced) {
- context.restore();
- }
-
- return canvas;
- }
-
- function getTouchesCenter(touches) {
- var length = touches.length;
- var pageX = 0;
- var pageY = 0;
-
- if (length) {
- $.each(touches, function (i, touch) {
- pageX += touch.pageX;
- pageY += touch.pageY;
- });
-
- pageX /= length;
- pageY /= length;
- }
-
- return {
- pageX: pageX,
- pageY: pageY
- };
- }
-
- function getStringFromCharCode(dataView, start, length) {
- var str = '';
- var i;
-
- for (i = start, length += start; i < length; i++) {
- str += fromCharCode(dataView.getUint8(i));
- }
-
- return str;
- }
-
- function getOrientation(arrayBuffer) {
- var dataView = new DataView(arrayBuffer);
- var length = dataView.byteLength;
- var orientation;
- var exifIDCode;
- var tiffOffset;
- var firstIFDOffset;
- var littleEndian;
- var endianness;
- var app1Start;
- var ifdStart;
- var offset;
- var i;
-
- // Only handle JPEG image (start by 0xFFD8)
- if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
- offset = 2;
-
- while (offset < length) {
- if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
- app1Start = offset;
- break;
- }
-
- offset++;
- }
- }
-
- if (app1Start) {
- exifIDCode = app1Start + 4;
- tiffOffset = app1Start + 10;
-
- if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
- endianness = dataView.getUint16(tiffOffset);
- littleEndian = endianness === 0x4949;
-
- if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
- if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
- firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
-
- if (firstIFDOffset >= 0x00000008) {
- ifdStart = tiffOffset + firstIFDOffset;
- }
- }
- }
- }
- }
-
- if (ifdStart) {
- length = dataView.getUint16(ifdStart, littleEndian);
-
- for (i = 0; i < length; i++) {
- offset = ifdStart + i * 12 + 2;
-
- if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
-
- // 8 is the offset of the current tag's value
- offset += 8;
-
- // Get the original orientation value
- orientation = dataView.getUint16(offset, littleEndian);
-
- // Override the orientation with the default value: 1
- dataView.setUint16(offset, 1, littleEndian);
- break;
- }
- }
- }
-
- return orientation;
- }
-
- function dataURLToArrayBuffer(dataURL) {
- var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');
- var binary = atob(base64);
- var length = binary.length;
- var arrayBuffer = new ArrayBuffer(length);
- var dataView = new Uint8Array(arrayBuffer);
- var i;
-
- for (i = 0; i < length; i++) {
- dataView[i] = binary.charCodeAt(i);
- }
-
- return arrayBuffer;
- }
-
- // Only available for JPEG image
- function arrayBufferToDataURL(arrayBuffer) {
- var dataView = new Uint8Array(arrayBuffer);
- var length = dataView.length;
- var base64 = '';
- var i;
-
- for (i = 0; i < length; i++) {
- base64 += fromCharCode(dataView[i]);
- }
-
- return 'data:image/jpeg;base64,' + btoa(base64);
- }
-
- function Cropper(element, options) {
- this.$element = $(element);
- this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options);
- this.isLoaded = false;
- this.isBuilt = false;
- this.isCompleted = false;
- this.isRotated = false;
- this.isCropped = false;
- this.isDisabled = false;
- this.isReplaced = false;
- this.isLimited = false;
- this.wheeling = false;
- this.isImg = false;
- this.originalUrl = '';
- this.canvas = null;
- this.cropBox = null;
- this.init();
- }
-
- Cropper.prototype = {
- constructor: Cropper,
-
- init: function () {
- var $this = this.$element;
- var url;
-
- if ($this.is('img')) {
- this.isImg = true;
-
- // Should use `$.fn.attr` here. e.g.: "img/picture.jpg"
- this.originalUrl = url = $this.attr('src');
-
- // Stop when it's a blank image
- if (!url) {
- return;
- }
-
- // Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg"
- url = $this.prop('src');
- } else if ($this.is('canvas') && SUPPORT_CANVAS) {
- url = $this[0].toDataURL();
- }
-
- this.load(url);
- },
-
- // A shortcut for triggering custom events
- trigger: function (type, data) {
- var e = $.Event(type, data);
-
- this.$element.trigger(e);
-
- return e;
- },
-
- load: function (url) {
- var options = this.options;
- var $this = this.$element;
- var read;
- var xhr;
-
- if (!url) {
- return;
- }
-
- // Trigger build event first
- $this.one(EVENT_BUILD, options.build);
-
- if (this.trigger(EVENT_BUILD).isDefaultPrevented()) {
- return;
- }
-
- this.url = url;
- this.image = {};
-
- if (!options.checkOrientation || !ArrayBuffer) {
- return this.clone();
- }
-
- read = $.proxy(this.read, this);
-
- // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
- if (REGEXP_DATA_URL.test(url)) {
- return REGEXP_DATA_URL_JPEG.test(url) ?
- read(dataURLToArrayBuffer(url)) :
- this.clone();
- }
-
- xhr = new XMLHttpRequest();
-
- xhr.onerror = xhr.onabort = $.proxy(function () {
- this.clone();
- }, this);
-
- xhr.onload = function () {
- read(this.response);
- };
-
- xhr.open('get', url);
- xhr.responseType = 'arraybuffer';
- xhr.send();
- },
-
- read: function (arrayBuffer) {
- var options = this.options;
- var orientation = getOrientation(arrayBuffer);
- var image = this.image;
- var rotate;
- var scaleX;
- var scaleY;
-
- if (orientation > 1) {
- this.url = arrayBufferToDataURL(arrayBuffer);
-
- switch (orientation) {
-
- // flip horizontal
- case 2:
- scaleX = -1;
- break;
-
- // rotate left 180°
- case 3:
- rotate = -180;
- break;
-
- // flip vertical
- case 4:
- scaleY = -1;
- break;
-
- // flip vertical + rotate right 90°
- case 5:
- rotate = 90;
- scaleY = -1;
- break;
-
- // rotate right 90°
- case 6:
- rotate = 90;
- break;
-
- // flip horizontal + rotate right 90°
- case 7:
- rotate = 90;
- scaleX = -1;
- break;
-
- // rotate left 90°
- case 8:
- rotate = -90;
- break;
- }
- }
-
- if (options.rotatable) {
- image.rotate = rotate;
- }
-
- if (options.scalable) {
- image.scaleX = scaleX;
- image.scaleY = scaleY;
- }
-
- this.clone();
- },
-
- clone: function () {
- var options = this.options;
- var $this = this.$element;
- var url = this.url;
- var crossOrigin = '';
- var crossOriginUrl;
- var $clone;
-
- if (options.checkCrossOrigin && isCrossOriginURL(url)) {
- crossOrigin = $this.prop('crossOrigin');
-
- if (crossOrigin) {
- crossOriginUrl = url;
- } else {
- crossOrigin = 'anonymous';
-
- // Bust cache (#148) when there is not a "crossOrigin" property
- crossOriginUrl = addTimestamp(url);
- }
- }
-
- this.crossOrigin = crossOrigin;
- this.crossOriginUrl = crossOriginUrl;
- this.$clone = $clone = $('<img' + getCrossOrigin(crossOrigin) + ' src="' + (crossOriginUrl || url) + '">');
-
- if (this.isImg) {
- if ($this[0].complete) {
- this.start();
- } else {
- $this.one(EVENT_LOAD, $.proxy(this.start, this));
- }
- } else {
- $clone.
- one(EVENT_LOAD, $.proxy(this.start, this)).
- one(EVENT_ERROR, $.proxy(this.stop, this)).
- addClass(CLASS_HIDE).
- insertAfter($this);
- }
- },
-
- start: function () {
- var $image = this.$element;
- var $clone = this.$clone;
-
- if (!this.isImg) {
- $clone.off(EVENT_ERROR, this.stop);
- $image = $clone;
- }
-
- getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) {
- $.extend(this.image, {
- naturalWidth: naturalWidth,
- naturalHeight: naturalHeight,
- aspectRatio: naturalWidth / naturalHeight
- });
-
- this.isLoaded = true;
- this.build();
- }, this));
- },
-
- stop: function () {
- this.$clone.remove();
- this.$clone = null;
- },
-
- build: function () {
- var options = this.options;
- var $this = this.$element;
- var $clone = this.$clone;
- var $cropper;
- var $cropBox;
- var $face;
-
- if (!this.isLoaded) {
- return;
- }
-
- // Unbuild first when replace
- if (this.isBuilt) {
- this.unbuild();
- }
-
- // Create cropper elements
- this.$container = $this.parent();
- this.$cropper = $cropper = $(Cropper.TEMPLATE);
- this.$canvas = $cropper.find('.cropper-canvas').append($clone);
- this.$dragBox = $cropper.find('.cropper-drag-box');
- this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box');
- this.$viewBox = $cropper.find('.cropper-view-box');
- this.$face = $face = $cropBox.find('.cropper-face');
-
- // Hide the original image
- $this.addClass(CLASS_HIDDEN).after($cropper);
-
- // Show the clone image if is hidden
- if (!this.isImg) {
- $clone.removeClass(CLASS_HIDE);
- }
-
- this.initPreview();
- this.bind();
-
- options.aspectRatio = max(0, options.aspectRatio) || NaN;
- options.viewMode = max(0, min(3, round(options.viewMode))) || 0;
-
- if (options.autoCrop) {
- this.isCropped = true;
-
- if (options.modal) {
- this.$dragBox.addClass(CLASS_MODAL);
- }
- } else {
- $cropBox.addClass(CLASS_HIDDEN);
- }
-
- if (!options.guides) {
- $cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN);
- }
-
- if (!options.center) {
- $cropBox.find('.cropper-center').addClass(CLASS_HIDDEN);
- }
-
- if (options.cropBoxMovable) {
- $face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL);
- }
-
- if (!options.highlight) {
- $face.addClass(CLASS_INVISIBLE);
- }
-
- if (options.background) {
- $cropper.addClass(CLASS_BG);
- }
-
- if (!options.cropBoxResizable) {
- $cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN);
- }
-
- this.setDragMode(options.dragMode);
- this.render();
- this.isBuilt = true;
- this.setData(options.data);
- $this.one(EVENT_BUILT, options.built);
-
- // Trigger the built event asynchronously to keep `data('cropper')` is defined
- setTimeout($.proxy(function () {
- this.trigger(EVENT_BUILT);
- this.isCompleted = true;
- }, this), 0);
- },
-
- unbuild: function () {
- if (!this.isBuilt) {
- return;
- }
-
- this.isBuilt = false;
- this.isCompleted = false;
- this.initialImage = null;
-
- // Clear `initialCanvas` is necessary when replace
- this.initialCanvas = null;
- this.initialCropBox = null;
- this.container = null;
- this.canvas = null;
-
- // Clear `cropBox` is necessary when replace
- this.cropBox = null;
- this.unbind();
-
- this.resetPreview();
- this.$preview = null;
-
- this.$viewBox = null;
- this.$cropBox = null;
- this.$dragBox = null;
- this.$canvas = null;
- this.$container = null;
-
- this.$cropper.remove();
- this.$cropper = null;
- },
-
- render: function () {
- this.initContainer();
- this.initCanvas();
- this.initCropBox();
-
- this.renderCanvas();
-
- if (this.isCropped) {
- this.renderCropBox();
- }
- },
-
- initContainer: function () {
- var options = this.options;
- var $this = this.$element;
- var $container = this.$container;
- var $cropper = this.$cropper;
-
- $cropper.addClass(CLASS_HIDDEN);
- $this.removeClass(CLASS_HIDDEN);
-
- $cropper.css((this.container = {
- width: max($container.width(), num(options.minContainerWidth) || 200),
- height: max($container.height(), num(options.minContainerHeight) || 100)
- }));
-
- $this.addClass(CLASS_HIDDEN);
- $cropper.removeClass(CLASS_HIDDEN);
- },
-
- // Canvas (image wrapper)
- initCanvas: function () {
- var viewMode = this.options.viewMode;
- var container = this.container;
- var containerWidth = container.width;
- var containerHeight = container.height;
- var image = this.image;
- var imageNaturalWidth = image.naturalWidth;
- var imageNaturalHeight = image.naturalHeight;
- var is90Degree = abs(image.rotate) === 90;
- var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth;
- var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight;
- var aspectRatio = naturalWidth / naturalHeight;
- var canvasWidth = containerWidth;
- var canvasHeight = containerHeight;
- var canvas;
-
- if (containerHeight * aspectRatio > containerWidth) {
- if (viewMode === 3) {
- canvasWidth = containerHeight * aspectRatio;
- } else {
- canvasHeight = containerWidth / aspectRatio;
- }
- } else {
- if (viewMode === 3) {
- canvasHeight = containerWidth / aspectRatio;
- } else {
- canvasWidth = containerHeight * aspectRatio;
- }
- }
-
- canvas = {
- naturalWidth: naturalWidth,
- naturalHeight: naturalHeight,
- aspectRatio: aspectRatio,
- width: canvasWidth,
- height: canvasHeight
- };
-
- canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2;
- canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2;
-
- this.canvas = canvas;
- this.isLimited = (viewMode === 1 || viewMode === 2);
- this.limitCanvas(true, true);
- this.initialImage = $.extend({}, image);
- this.initialCanvas = $.extend({}, canvas);
- },
-
- limitCanvas: function (isSizeLimited, isPositionLimited) {
- var options = this.options;
- var viewMode = options.viewMode;
- var container = this.container;
- var containerWidth = container.width;
- var containerHeight = container.height;
- var canvas = this.canvas;
- var aspectRatio = canvas.aspectRatio;
- var cropBox = this.cropBox;
- var isCropped = this.isCropped && cropBox;
- var minCanvasWidth;
- var minCanvasHeight;
- var newCanvasLeft;
- var newCanvasTop;
-
- if (isSizeLimited) {
- minCanvasWidth = num(options.minCanvasWidth) || 0;
- minCanvasHeight = num(options.minCanvasHeight) || 0;
-
- if (viewMode) {
- if (viewMode > 1) {
- minCanvasWidth = max(minCanvasWidth, containerWidth);
- minCanvasHeight = max(minCanvasHeight, containerHeight);
-
- if (viewMode === 3) {
- if (minCanvasHeight * aspectRatio > minCanvasWidth) {
- minCanvasWidth = minCanvasHeight * aspectRatio;
- } else {
- minCanvasHeight = minCanvasWidth / aspectRatio;
- }
- }
- } else {
- if (minCanvasWidth) {
- minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0);
- } else if (minCanvasHeight) {
- minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0);
- } else if (isCropped) {
- minCanvasWidth = cropBox.width;
- minCanvasHeight = cropBox.height;
-
- if (minCanvasHeight * aspectRatio > minCanvasWidth) {
- minCanvasWidth = minCanvasHeight * aspectRatio;
- } else {
- minCanvasHeight = minCanvasWidth / aspectRatio;
- }
- }
- }
- }
-
- if (minCanvasWidth && minCanvasHeight) {
- if (minCanvasHeight * aspectRatio > minCanvasWidth) {
- minCanvasHeight = minCanvasWidth / aspectRatio;
- } else {
- minCanvasWidth = minCanvasHeight * aspectRatio;
- }
- } else if (minCanvasWidth) {
- minCanvasHeight = minCanvasWidth / aspectRatio;
- } else if (minCanvasHeight) {
- minCanvasWidth = minCanvasHeight * aspectRatio;
- }
-
- canvas.minWidth = minCanvasWidth;
- canvas.minHeight = minCanvasHeight;
- canvas.maxWidth = Infinity;
- canvas.maxHeight = Infinity;
- }
-
- if (isPositionLimited) {
- if (viewMode) {
- newCanvasLeft = containerWidth - canvas.width;
- newCanvasTop = containerHeight - canvas.height;
-
- canvas.minLeft = min(0, newCanvasLeft);
- canvas.minTop = min(0, newCanvasTop);
- canvas.maxLeft = max(0, newCanvasLeft);
- canvas.maxTop = max(0, newCanvasTop);
-
- if (isCropped && this.isLimited) {
- canvas.minLeft = min(
- cropBox.left,
- cropBox.left + cropBox.width - canvas.width
- );
- canvas.minTop = min(
- cropBox.top,
- cropBox.top + cropBox.height - canvas.height
- );
- canvas.maxLeft = cropBox.left;
- canvas.maxTop = cropBox.top;
-
- if (viewMode === 2) {
- if (canvas.width >= containerWidth) {
- canvas.minLeft = min(0, newCanvasLeft);
- canvas.maxLeft = max(0, newCanvasLeft);
- }
-
- if (canvas.height >= containerHeight) {
- canvas.minTop = min(0, newCanvasTop);
- canvas.maxTop = max(0, newCanvasTop);
- }
- }
- }
- } else {
- canvas.minLeft = -canvas.width;
- canvas.minTop = -canvas.height;
- canvas.maxLeft = containerWidth;
- canvas.maxTop = containerHeight;
- }
- }
- },
-
- renderCanvas: function (isChanged) {
- var canvas = this.canvas;
- var image = this.image;
- var rotate = image.rotate;
- var naturalWidth = image.naturalWidth;
- var naturalHeight = image.naturalHeight;
- var aspectRatio;
- var rotated;
-
- if (this.isRotated) {
- this.isRotated = false;
-
- // Computes rotated sizes with image sizes
- rotated = getRotatedSizes({
- width: image.width,
- height: image.height,
- degree: rotate
- });
-
- aspectRatio = rotated.width / rotated.height;
-
- if (aspectRatio !== canvas.aspectRatio) {
- canvas.left -= (rotated.width - canvas.width) / 2;
- canvas.top -= (rotated.height - canvas.height) / 2;
- canvas.width = rotated.width;
- canvas.height = rotated.height;
- canvas.aspectRatio = aspectRatio;
- canvas.naturalWidth = naturalWidth;
- canvas.naturalHeight = naturalHeight;
-
- // Computes rotated sizes with natural image sizes
- if (rotate % 180) {
- rotated = getRotatedSizes({
- width: naturalWidth,
- height: naturalHeight,
- degree: rotate
- });
-
- canvas.naturalWidth = rotated.width;
- canvas.naturalHeight = rotated.height;
- }
-
- this.limitCanvas(true, false);
- }
- }
-
- if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) {
- canvas.left = canvas.oldLeft;
- }
-
- if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) {
- canvas.top = canvas.oldTop;
- }
-
- canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth);
- canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight);
-
- this.limitCanvas(false, true);
-
- canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft);
- canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop);
-
- this.$canvas.css({
- width: canvas.width,
- height: canvas.height,
- left: canvas.left,
- top: canvas.top
- });
-
- this.renderImage();
-
- if (this.isCropped && this.isLimited) {
- this.limitCropBox(true, true);
- }
-
- if (isChanged) {
- this.output();
- }
- },
-
- renderImage: function (isChanged) {
- var canvas = this.canvas;
- var image = this.image;
- var reversed;
-
- if (image.rotate) {
- reversed = getRotatedSizes({
- width: canvas.width,
- height: canvas.height,
- degree: image.rotate,
- aspectRatio: image.aspectRatio
- }, true);
- }
-
- $.extend(image, reversed ? {
- width: reversed.width,
- height: reversed.height,
- left: (canvas.width - reversed.width) / 2,
- top: (canvas.height - reversed.height) / 2
- } : {
- width: canvas.width,
- height: canvas.height,
- left: 0,
- top: 0
- });
-
- this.$clone.css({
- width: image.width,
- height: image.height,
- marginLeft: image.left,
- marginTop: image.top,
- transform: getTransform(image)
- });
-
- if (isChanged) {
- this.output();
- }
- },
-
- initCropBox: function () {
- var options = this.options;
- var canvas = this.canvas;
- var aspectRatio = options.aspectRatio;
- var autoCropArea = num(options.autoCropArea) || 0.8;
- var cropBox = {
- width: canvas.width,
- height: canvas.height
- };
-
- if (aspectRatio) {
- if (canvas.height * aspectRatio > canvas.width) {
- cropBox.height = cropBox.width / aspectRatio;
- } else {
- cropBox.width = cropBox.height * aspectRatio;
- }
- }
-
- this.cropBox = cropBox;
- this.limitCropBox(true, true);
-
- // Initialize auto crop area
- cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
- cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
-
- // The width of auto crop area must large than "minWidth", and the height too. (#164)
- cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea);
- cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea);
- cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2;
- cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2;
-
- this.initialCropBox = $.extend({}, cropBox);
- },
-
- limitCropBox: function (isSizeLimited, isPositionLimited) {
- var options = this.options;
- var aspectRatio = options.aspectRatio;
- var container = this.container;
- var containerWidth = container.width;
- var containerHeight = container.height;
- var canvas = this.canvas;
- var cropBox = this.cropBox;
- var isLimited = this.isLimited;
- var minCropBoxWidth;
- var minCropBoxHeight;
- var maxCropBoxWidth;
- var maxCropBoxHeight;
-
- if (isSizeLimited) {
- minCropBoxWidth = num(options.minCropBoxWidth) || 0;
- minCropBoxHeight = num(options.minCropBoxHeight) || 0;
-
- // The min/maxCropBoxWidth/Height must be less than containerWidth/Height
- minCropBoxWidth = min(minCropBoxWidth, containerWidth);
- minCropBoxHeight = min(minCropBoxHeight, containerHeight);
- maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth);
- maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight);
-
- if (aspectRatio) {
- if (minCropBoxWidth && minCropBoxHeight) {
- if (minCropBoxHeight * aspectRatio > minCropBoxWidth) {
- minCropBoxHeight = minCropBoxWidth / aspectRatio;
- } else {
- minCropBoxWidth = minCropBoxHeight * aspectRatio;
- }
- } else if (minCropBoxWidth) {
- minCropBoxHeight = minCropBoxWidth / aspectRatio;
- } else if (minCropBoxHeight) {
- minCropBoxWidth = minCropBoxHeight * aspectRatio;
- }
-
- if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) {
- maxCropBoxHeight = maxCropBoxWidth / aspectRatio;
- } else {
- maxCropBoxWidth = maxCropBoxHeight * aspectRatio;
- }
- }
-
- // The minWidth/Height must be less than maxWidth/Height
- cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth);
- cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight);
- cropBox.maxWidth = maxCropBoxWidth;
- cropBox.maxHeight = maxCropBoxHeight;
- }
-
- if (isPositionLimited) {
- if (isLimited) {
- cropBox.minLeft = max(0, canvas.left);
- cropBox.minTop = max(0, canvas.top);
- cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width;
- cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height;
- } else {
- cropBox.minLeft = 0;
- cropBox.minTop = 0;
- cropBox.maxLeft = containerWidth - cropBox.width;
- cropBox.maxTop = containerHeight - cropBox.height;
- }
- }
- },
-
- renderCropBox: function () {
- var options = this.options;
- var container = this.container;
- var containerWidth = container.width;
- var containerHeight = container.height;
- var cropBox = this.cropBox;
-
- if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) {
- cropBox.left = cropBox.oldLeft;
- }
-
- if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) {
- cropBox.top = cropBox.oldTop;
- }
-
- cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
- cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
-
- this.limitCropBox(false, true);
-
- cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft);
- cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop);
-
- if (options.movable && options.cropBoxMovable) {
-
- // Turn to move the canvas when the crop box is equal to the container
- this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL);
- }
-
- this.$cropBox.css({
- width: cropBox.width,
- height: cropBox.height,
- left: cropBox.left,
- top: cropBox.top
- });
-
- if (this.isCropped && this.isLimited) {
- this.limitCanvas(true, true);
- }
-
- if (!this.isDisabled) {
- this.output();
- }
- },
-
- output: function () {
- this.preview();
-
- if (this.isCompleted) {
- this.trigger(EVENT_CROP, this.getData());
- } else if (!this.isBuilt) {
-
- // Only trigger one crop event before complete
- this.$element.one(EVENT_BUILT, $.proxy(function () {
- this.trigger(EVENT_CROP, this.getData());
- }, this));
- }
- },
-
- initPreview: function () {
- var crossOrigin = getCrossOrigin(this.crossOrigin);
- var url = crossOrigin ? this.crossOriginUrl : this.url;
-
- this.$preview = $(this.options.preview);
- this.$viewBox.html('<img' + crossOrigin + ' src="' + url + '">');
- this.$preview.each(function () {
- var $this = $(this);
-
- // Save the original size for recover
- $this.data(DATA_PREVIEW, {
- width: $this.width(),
- height: $this.height(),
- html: $this.html()
- });
-
- /**
- * Override img element styles
- * Add `display:block` to avoid margin top issue
- * (Occur only when margin-top <= -height)
- */
- $this.html(
- '<img' + crossOrigin + ' src="' + url + '" style="' +
- 'display:block;width:100%;height:auto;' +
- 'min-width:0!important;min-height:0!important;' +
- 'max-width:none!important;max-height:none!important;' +
- 'image-orientation:0deg!important;">'
- );
- });
- },
-
- resetPreview: function () {
- this.$preview.each(function () {
- var $this = $(this);
- var data = $this.data(DATA_PREVIEW);
-
- $this.css({
- width: data.width,
- height: data.height
- }).html(data.html).removeData(DATA_PREVIEW);
- });
- },
-
- preview: function () {
- var image = this.image;
- var canvas = this.canvas;
- var cropBox = this.cropBox;
- var cropBoxWidth = cropBox.width;
- var cropBoxHeight = cropBox.height;
- var width = image.width;
- var height = image.height;
- var left = cropBox.left - canvas.left - image.left;
- var top = cropBox.top - canvas.top - image.top;
-
- if (!this.isCropped || this.isDisabled) {
- return;
- }
-
- this.$viewBox.find('img').css({
- width: width,
- height: height,
- marginLeft: -left,
- marginTop: -top,
- transform: getTransform(image)
- });
-
- this.$preview.each(function () {
- var $this = $(this);
- var data = $this.data(DATA_PREVIEW);
- var originalWidth = data.width;
- var originalHeight = data.height;
- var newWidth = originalWidth;
- var newHeight = originalHeight;
- var ratio = 1;
-
- if (cropBoxWidth) {
- ratio = originalWidth / cropBoxWidth;
- newHeight = cropBoxHeight * ratio;
- }
-
- if (cropBoxHeight && newHeight > originalHeight) {
- ratio = originalHeight / cropBoxHeight;
- newWidth = cropBoxWidth * ratio;
- newHeight = originalHeight;
- }
-
- $this.css({
- width: newWidth,
- height: newHeight
- }).find('img').css({
- width: width * ratio,
- height: height * ratio,
- marginLeft: -left * ratio,
- marginTop: -top * ratio,
- transform: getTransform(image)
- });
- });
- },
-
- bind: function () {
- var options = this.options;
- var $this = this.$element;
- var $cropper = this.$cropper;
-
- if ($.isFunction(options.cropstart)) {
- $this.on(EVENT_CROP_START, options.cropstart);
- }
-
- if ($.isFunction(options.cropmove)) {
- $this.on(EVENT_CROP_MOVE, options.cropmove);
- }
-
- if ($.isFunction(options.cropend)) {
- $this.on(EVENT_CROP_END, options.cropend);
- }
-
- if ($.isFunction(options.crop)) {
- $this.on(EVENT_CROP, options.crop);
- }
-
- if ($.isFunction(options.zoom)) {
- $this.on(EVENT_ZOOM, options.zoom);
- }
-
- $cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this));
-
- if (options.zoomable && options.zoomOnWheel) {
- $cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this));
- }
-
- if (options.toggleDragModeOnDblclick) {
- $cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this));
- }
-
- $document.
- on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))).
- on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this)));
-
- if (options.responsive) {
- $window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this)));
- }
- },
-
- unbind: function () {
- var options = this.options;
- var $this = this.$element;
- var $cropper = this.$cropper;
-
- if ($.isFunction(options.cropstart)) {
- $this.off(EVENT_CROP_START, options.cropstart);
- }
-
- if ($.isFunction(options.cropmove)) {
- $this.off(EVENT_CROP_MOVE, options.cropmove);
- }
-
- if ($.isFunction(options.cropend)) {
- $this.off(EVENT_CROP_END, options.cropend);
- }
-
- if ($.isFunction(options.crop)) {
- $this.off(EVENT_CROP, options.crop);
- }
-
- if ($.isFunction(options.zoom)) {
- $this.off(EVENT_ZOOM, options.zoom);
- }
-
- $cropper.off(EVENT_MOUSE_DOWN, this.cropStart);
-
- if (options.zoomable && options.zoomOnWheel) {
- $cropper.off(EVENT_WHEEL, this.wheel);
- }
-
- if (options.toggleDragModeOnDblclick) {
- $cropper.off(EVENT_DBLCLICK, this.dblclick);
- }
-
- $document.
- off(EVENT_MOUSE_MOVE, this._cropMove).
- off(EVENT_MOUSE_UP, this._cropEnd);
-
- if (options.responsive) {
- $window.off(EVENT_RESIZE, this._resize);
- }
- },
-
- resize: function () {
- var restore = this.options.restore;
- var $container = this.$container;
- var container = this.container;
- var canvasData;
- var cropBoxData;
- var ratio;
-
- // Check `container` is necessary for IE8
- if (this.isDisabled || !container) {
- return;
- }
-
- ratio = $container.width() / container.width;
-
- // Resize when width changed or height changed
- if (ratio !== 1 || $container.height() !== container.height) {
- if (restore) {
- canvasData = this.getCanvasData();
- cropBoxData = this.getCropBoxData();
- }
-
- this.render();
-
- if (restore) {
- this.setCanvasData($.each(canvasData, function (i, n) {
- canvasData[i] = n * ratio;
- }));
- this.setCropBoxData($.each(cropBoxData, function (i, n) {
- cropBoxData[i] = n * ratio;
- }));
- }
- }
- },
-
- dblclick: function () {
- if (this.isDisabled) {
- return;
- }
-
- if (this.$dragBox.hasClass(CLASS_CROP)) {
- this.setDragMode(ACTION_MOVE);
- } else {
- this.setDragMode(ACTION_CROP);
- }
- },
-
- wheel: function (event) {
- var e = event.originalEvent || event;
- var ratio = num(this.options.wheelZoomRatio) || 0.1;
- var delta = 1;
-
- if (this.isDisabled) {
- return;
- }
-
- event.preventDefault();
-
- // Limit wheel speed to prevent zoom too fast
- if (this.wheeling) {
- return;
- }
-
- this.wheeling = true;
-
- setTimeout($.proxy(function () {
- this.wheeling = false;
- }, this), 50);
-
- if (e.deltaY) {
- delta = e.deltaY > 0 ? 1 : -1;
- } else if (e.wheelDelta) {
- delta = -e.wheelDelta / 120;
- } else if (e.detail) {
- delta = e.detail > 0 ? 1 : -1;
- }
-
- this.zoom(-delta * ratio, event);
- },
-
- cropStart: function (event) {
- var options = this.options;
- var originalEvent = event.originalEvent;
- var touches = originalEvent && originalEvent.touches;
- var e = event;
- var touchesLength;
- var action;
-
- if (this.isDisabled) {
- return;
- }
-
- if (touches) {
- touchesLength = touches.length;
-
- if (touchesLength > 1) {
- if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
- e = touches[1];
- this.startX2 = e.pageX;
- this.startY2 = e.pageY;
- action = ACTION_ZOOM;
- } else {
- return;
- }
- }
-
- e = touches[0];
- }
-
- action = action || $(e.target).data(DATA_ACTION);
-
- if (REGEXP_ACTIONS.test(action)) {
- if (this.trigger(EVENT_CROP_START, {
- originalEvent: originalEvent,
- action: action
- }).isDefaultPrevented()) {
- return;
- }
-
- event.preventDefault();
-
- this.action = action;
- this.cropping = false;
-
- // IE8 has `event.pageX/Y`, but not `event.originalEvent.pageX/Y`
- // IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y`
- this.startX = e.pageX || originalEvent && originalEvent.pageX;
- this.startY = e.pageY || originalEvent && originalEvent.pageY;
-
- if (action === ACTION_CROP) {
- this.cropping = true;
- this.$dragBox.addClass(CLASS_MODAL);
- }
- }
- },
-
- cropMove: function (event) {
- var options = this.options;
- var originalEvent = event.originalEvent;
- var touches = originalEvent && originalEvent.touches;
- var e = event;
- var action = this.action;
- var touchesLength;
-
- if (this.isDisabled) {
- return;
- }
-
- if (touches) {
- touchesLength = touches.length;
-
- if (touchesLength > 1) {
- if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
- e = touches[1];
- this.endX2 = e.pageX;
- this.endY2 = e.pageY;
- } else {
- return;
- }
- }
-
- e = touches[0];
- }
-
- if (action) {
- if (this.trigger(EVENT_CROP_MOVE, {
- originalEvent: originalEvent,
- action: action
- }).isDefaultPrevented()) {
- return;
- }
-
- event.preventDefault();
-
- this.endX = e.pageX || originalEvent && originalEvent.pageX;
- this.endY = e.pageY || originalEvent && originalEvent.pageY;
-
- this.change(e.shiftKey, action === ACTION_ZOOM ? event : null);
- }
- },
-
- cropEnd: function (event) {
- var originalEvent = event.originalEvent;
- var action = this.action;
-
- if (this.isDisabled) {
- return;
- }
-
- if (action) {
- event.preventDefault();
-
- if (this.cropping) {
- this.cropping = false;
- this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal);
- }
-
- this.action = '';
-
- this.trigger(EVENT_CROP_END, {
- originalEvent: originalEvent,
- action: action
- });
- }
- },
-
- change: function (shiftKey, event) {
- var options = this.options;
- var aspectRatio = options.aspectRatio;
- var action = this.action;
- var container = this.container;
- var canvas = this.canvas;
- var cropBox = this.cropBox;
- var width = cropBox.width;
- var height = cropBox.height;
- var left = cropBox.left;
- var top = cropBox.top;
- var right = left + width;
- var bottom = top + height;
- var minLeft = 0;
- var minTop = 0;
- var maxWidth = container.width;
- var maxHeight = container.height;
- var renderable = true;
- var offset;
- var range;
-
- // Locking aspect ratio in "free mode" by holding shift key (#259)
- if (!aspectRatio && shiftKey) {
- aspectRatio = width && height ? width / height : 1;
- }
-
- if (this.limited) {
- minLeft = cropBox.minLeft;
- minTop = cropBox.minTop;
- maxWidth = minLeft + min(container.width, canvas.width);
- maxHeight = minTop + min(container.height, canvas.height);
- }
-
- range = {
- x: this.endX - this.startX,
- y: this.endY - this.startY
- };
-
- if (aspectRatio) {
- range.X = range.y * aspectRatio;
- range.Y = range.x / aspectRatio;
- }
-
- switch (action) {
- // Move crop box
- case ACTION_ALL:
- left += range.x;
- top += range.y;
- break;
-
- // Resize crop box
- case ACTION_EAST:
- if (range.x >= 0 && (right >= maxWidth || aspectRatio &&
- (top <= minTop || bottom >= maxHeight))) {
-
- renderable = false;
- break;
- }
-
- width += range.x;
-
- if (aspectRatio) {
- height = width / aspectRatio;
- top -= range.Y / 2;
- }
-
- if (width < 0) {
- action = ACTION_WEST;
- width = 0;
- }
-
- break;
-
- case ACTION_NORTH:
- if (range.y <= 0 && (top <= minTop || aspectRatio &&
- (left <= minLeft || right >= maxWidth))) {
-
- renderable = false;
- break;
- }
-
- height -= range.y;
- top += range.y;
-
- if (aspectRatio) {
- width = height * aspectRatio;
- left += range.X / 2;
- }
-
- if (height < 0) {
- action = ACTION_SOUTH;
- height = 0;
- }
-
- break;
-
- case ACTION_WEST:
- if (range.x <= 0 && (left <= minLeft || aspectRatio &&
- (top <= minTop || bottom >= maxHeight))) {
-
- renderable = false;
- break;
- }
-
- width -= range.x;
- left += range.x;
-
- if (aspectRatio) {
- height = width / aspectRatio;
- top += range.Y / 2;
- }
-
- if (width < 0) {
- action = ACTION_EAST;
- width = 0;
- }
-
- break;
-
- case ACTION_SOUTH:
- if (range.y >= 0 && (bottom >= maxHeight || aspectRatio &&
- (left <= minLeft || right >= maxWidth))) {
-
- renderable = false;
- break;
- }
-
- height += range.y;
-
- if (aspectRatio) {
- width = height * aspectRatio;
- left -= range.X / 2;
- }
-
- if (height < 0) {
- action = ACTION_NORTH;
- height = 0;
- }
-
- break;
-
- case ACTION_NORTH_EAST:
- if (aspectRatio) {
- if (range.y <= 0 && (top <= minTop || right >= maxWidth)) {
- renderable = false;
- break;
- }
-
- height -= range.y;
- top += range.y;
- width = height * aspectRatio;
- } else {
- if (range.x >= 0) {
- if (right < maxWidth) {
- width += range.x;
- } else if (range.y <= 0 && top <= minTop) {
- renderable = false;
- }
- } else {
- width += range.x;
- }
-
- if (range.y <= 0) {
- if (top > minTop) {
- height -= range.y;
- top += range.y;
- }
- } else {
- height -= range.y;
- top += range.y;
- }
- }
-
- if (width < 0 && height < 0) {
- action = ACTION_SOUTH_WEST;
- height = 0;
- width = 0;
- } else if (width < 0) {
- action = ACTION_NORTH_WEST;
- width = 0;
- } else if (height < 0) {
- action = ACTION_SOUTH_EAST;
- height = 0;
- }
-
- break;
-
- case ACTION_NORTH_WEST:
- if (aspectRatio) {
- if (range.y <= 0 && (top <= minTop || left <= minLeft)) {
- renderable = false;
- break;
- }
-
- height -= range.y;
- top += range.y;
- width = height * aspectRatio;
- left += range.X;
- } else {
- if (range.x <= 0) {
- if (left > minLeft) {
- width -= range.x;
- left += range.x;
- } else if (range.y <= 0 && top <= minTop) {
- renderable = false;
- }
- } else {
- width -= range.x;
- left += range.x;
- }
-
- if (range.y <= 0) {
- if (top > minTop) {
- height -= range.y;
- top += range.y;
- }
- } else {
- height -= range.y;
- top += range.y;
- }
- }
-
- if (width < 0 && height < 0) {
- action = ACTION_SOUTH_EAST;
- height = 0;
- width = 0;
- } else if (width < 0) {
- action = ACTION_NORTH_EAST;
- width = 0;
- } else if (height < 0) {
- action = ACTION_SOUTH_WEST;
- height = 0;
- }
-
- break;
-
- case ACTION_SOUTH_WEST:
- if (aspectRatio) {
- if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) {
- renderable = false;
- break;
- }
-
- width -= range.x;
- left += range.x;
- height = width / aspectRatio;
- } else {
- if (range.x <= 0) {
- if (left > minLeft) {
- width -= range.x;
- left += range.x;
- } else if (range.y >= 0 && bottom >= maxHeight) {
- renderable = false;
- }
- } else {
- width -= range.x;
- left += range.x;
- }
-
- if (range.y >= 0) {
- if (bottom < maxHeight) {
- height += range.y;
- }
- } else {
- height += range.y;
- }
- }
-
- if (width < 0 && height < 0) {
- action = ACTION_NORTH_EAST;
- height = 0;
- width = 0;
- } else if (width < 0) {
- action = ACTION_SOUTH_EAST;
- width = 0;
- } else if (height < 0) {
- action = ACTION_NORTH_WEST;
- height = 0;
- }
-
- break;
-
- case ACTION_SOUTH_EAST:
- if (aspectRatio) {
- if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) {
- renderable = false;
- break;
- }
-
- width += range.x;
- height = width / aspectRatio;
- } else {
- if (range.x >= 0) {
- if (right < maxWidth) {
- width += range.x;
- } else if (range.y >= 0 && bottom >= maxHeight) {
- renderable = false;
- }
- } else {
- width += range.x;
- }
-
- if (range.y >= 0) {
- if (bottom < maxHeight) {
- height += range.y;
- }
- } else {
- height += range.y;
- }
- }
-
- if (width < 0 && height < 0) {
- action = ACTION_NORTH_WEST;
- height = 0;
- width = 0;
- } else if (width < 0) {
- action = ACTION_SOUTH_WEST;
- width = 0;
- } else if (height < 0) {
- action = ACTION_NORTH_EAST;
- height = 0;
- }
-
- break;
-
- // Move canvas
- case ACTION_MOVE:
- this.move(range.x, range.y);
- renderable = false;
- break;
-
- // Zoom canvas
- case ACTION_ZOOM:
- this.zoom((function (x1, y1, x2, y2) {
- var z1 = sqrt(x1 * x1 + y1 * y1);
- var z2 = sqrt(x2 * x2 + y2 * y2);
-
- return (z2 - z1) / z1;
- })(
- abs(this.startX - this.startX2),
- abs(this.startY - this.startY2),
- abs(this.endX - this.endX2),
- abs(this.endY - this.endY2)
- ), event);
- this.startX2 = this.endX2;
- this.startY2 = this.endY2;
- renderable = false;
- break;
-
- // Create crop box
- case ACTION_CROP:
- if (!range.x || !range.y) {
- renderable = false;
- break;
- }
-
- offset = this.$cropper.offset();
- left = this.startX - offset.left;
- top = this.startY - offset.top;
- width = cropBox.minWidth;
- height = cropBox.minHeight;
-
- if (range.x > 0) {
- action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST;
- } else if (range.x < 0) {
- left -= width;
- action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST;
- }
-
- if (range.y < 0) {
- top -= height;
- }
-
- // Show the crop box if is hidden
- if (!this.isCropped) {
- this.$cropBox.removeClass(CLASS_HIDDEN);
- this.isCropped = true;
-
- if (this.limited) {
- this.limitCropBox(true, true);
- }
- }
-
- break;
-
- // No default
- }
-
- if (renderable) {
- cropBox.width = width;
- cropBox.height = height;
- cropBox.left = left;
- cropBox.top = top;
- this.action = action;
-
- this.renderCropBox();
- }
-
- // Override
- this.startX = this.endX;
- this.startY = this.endY;
- },
-
- // Show the crop box manually
- crop: function () {
- if (!this.isBuilt || this.isDisabled) {
- return;
- }
-
- if (!this.isCropped) {
- this.isCropped = true;
- this.limitCropBox(true, true);
-
- if (this.options.modal) {
- this.$dragBox.addClass(CLASS_MODAL);
- }
-
- this.$cropBox.removeClass(CLASS_HIDDEN);
- }
-
- this.setCropBoxData(this.initialCropBox);
- },
-
- // Reset the image and crop box to their initial states
- reset: function () {
- if (!this.isBuilt || this.isDisabled) {
- return;
- }
-
- this.image = $.extend({}, this.initialImage);
- this.canvas = $.extend({}, this.initialCanvas);
- this.cropBox = $.extend({}, this.initialCropBox);
-
- this.renderCanvas();
-
- if (this.isCropped) {
- this.renderCropBox();
- }
- },
-
- // Clear the crop box
- clear: function () {
- if (!this.isCropped || this.isDisabled) {
- return;
- }
-
- $.extend(this.cropBox, {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- });
-
- this.isCropped = false;
- this.renderCropBox();
-
- this.limitCanvas(true, true);
-
- // Render canvas after crop box rendered
- this.renderCanvas();
-
- this.$dragBox.removeClass(CLASS_MODAL);
- this.$cropBox.addClass(CLASS_HIDDEN);
- },
-
- /**
- * Replace the image's src and rebuild the cropper
- *
- * @param {String} url
- */
- replace: function (url) {
- if (!this.isDisabled && url) {
- if (this.isImg) {
- this.isReplaced = true;
- this.$element.attr('src', url);
- }
-
- // Clear previous data
- this.options.data = null;
- this.load(url);
- }
- },
-
- // Enable (unfreeze) the cropper
- enable: function () {
- if (this.isBuilt) {
- this.isDisabled = false;
- this.$cropper.removeClass(CLASS_DISABLED);
- }
- },
-
- // Disable (freeze) the cropper
- disable: function () {
- if (this.isBuilt) {
- this.isDisabled = true;
- this.$cropper.addClass(CLASS_DISABLED);
- }
- },
-
- // Destroy the cropper and remove the instance from the image
- destroy: function () {
- var $this = this.$element;
-
- if (this.isLoaded) {
- if (this.isImg && this.isReplaced) {
- $this.attr('src', this.originalUrl);
- }
-
- this.unbuild();
- $this.removeClass(CLASS_HIDDEN);
- } else {
- if (this.isImg) {
- $this.off(EVENT_LOAD, this.start);
- } else if (this.$clone) {
- this.$clone.remove();
- }
- }
-
- $this.removeData(NAMESPACE);
- },
-
- /**
- * Move the canvas with relative offsets
- *
- * @param {Number} offsetX
- * @param {Number} offsetY (optional)
- */
- move: function (offsetX, offsetY) {
- var canvas = this.canvas;
-
- this.moveTo(
- isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX),
- isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY)
- );
- },
-
- /**
- * Move the canvas to an absolute point
- *
- * @param {Number} x
- * @param {Number} y (optional)
- */
- moveTo: function (x, y) {
- var canvas = this.canvas;
- var isChanged = false;
-
- // If "y" is not present, its default value is "x"
- if (isUndefined(y)) {
- y = x;
- }
-
- x = num(x);
- y = num(y);
-
- if (this.isBuilt && !this.isDisabled && this.options.movable) {
- if (isNumber(x)) {
- canvas.left = x;
- isChanged = true;
- }
-
- if (isNumber(y)) {
- canvas.top = y;
- isChanged = true;
- }
-
- if (isChanged) {
- this.renderCanvas(true);
- }
- }
- },
-
- /**
- * Zoom the canvas with a relative ratio
- *
- * @param {Number} ratio
- * @param {jQuery Event} _event (private)
- */
- zoom: function (ratio, _event) {
- var canvas = this.canvas;
-
- ratio = num(ratio);
-
- if (ratio < 0) {
- ratio = 1 / (1 - ratio);
- } else {
- ratio = 1 + ratio;
- }
-
- this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event);
- },
-
- /**
- * Zoom the canvas to an absolute ratio
- *
- * @param {Number} ratio
- * @param {jQuery Event} _event (private)
- */
- zoomTo: function (ratio, _event) {
- var options = this.options;
- var canvas = this.canvas;
- var width = canvas.width;
- var height = canvas.height;
- var naturalWidth = canvas.naturalWidth;
- var naturalHeight = canvas.naturalHeight;
- var originalEvent;
- var newWidth;
- var newHeight;
- var offset;
- var center;
-
- ratio = num(ratio);
-
- if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) {
- newWidth = naturalWidth * ratio;
- newHeight = naturalHeight * ratio;
-
- if (_event) {
- originalEvent = _event.originalEvent;
- }
-
- if (this.trigger(EVENT_ZOOM, {
- originalEvent: originalEvent,
- oldRatio: width / naturalWidth,
- ratio: newWidth / naturalWidth
- }).isDefaultPrevented()) {
- return;
- }
-
- if (originalEvent) {
- offset = this.$cropper.offset();
- center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : {
- pageX: _event.pageX || originalEvent.pageX || 0,
- pageY: _event.pageY || originalEvent.pageY || 0
- };
-
- // Zoom from the triggering point of the event
- canvas.left -= (newWidth - width) * (
- ((center.pageX - offset.left) - canvas.left) / width
- );
- canvas.top -= (newHeight - height) * (
- ((center.pageY - offset.top) - canvas.top) / height
- );
- } else {
-
- // Zoom from the center of the canvas
- canvas.left -= (newWidth - width) / 2;
- canvas.top -= (newHeight - height) / 2;
- }
-
- canvas.width = newWidth;
- canvas.height = newHeight;
- this.renderCanvas(true);
- }
- },
-
- /**
- * Rotate the canvas with a relative degree
- *
- * @param {Number} degree
- */
- rotate: function (degree) {
- this.rotateTo((this.image.rotate || 0) + num(degree));
- },
-
- /**
- * Rotate the canvas to an absolute degree
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate()
- *
- * @param {Number} degree
- */
- rotateTo: function (degree) {
- degree = num(degree);
-
- if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) {
- this.image.rotate = degree % 360;
- this.isRotated = true;
- this.renderCanvas(true);
- }
- },
-
- /**
- * Scale the image
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale()
- *
- * @param {Number} scaleX
- * @param {Number} scaleY (optional)
- */
- scale: function (scaleX, scaleY) {
- var image = this.image;
- var isChanged = false;
-
- // If "scaleY" is not present, its default value is "scaleX"
- if (isUndefined(scaleY)) {
- scaleY = scaleX;
- }
-
- scaleX = num(scaleX);
- scaleY = num(scaleY);
-
- if (this.isBuilt && !this.isDisabled && this.options.scalable) {
- if (isNumber(scaleX)) {
- image.scaleX = scaleX;
- isChanged = true;
- }
-
- if (isNumber(scaleY)) {
- image.scaleY = scaleY;
- isChanged = true;
- }
-
- if (isChanged) {
- this.renderImage(true);
- }
- }
- },
-
- /**
- * Scale the abscissa of the image
- *
- * @param {Number} scaleX
- */
- scaleX: function (scaleX) {
- var scaleY = this.image.scaleY;
-
- this.scale(scaleX, isNumber(scaleY) ? scaleY : 1);
- },
-
- /**
- * Scale the ordinate of the image
- *
- * @param {Number} scaleY
- */
- scaleY: function (scaleY) {
- var scaleX = this.image.scaleX;
-
- this.scale(isNumber(scaleX) ? scaleX : 1, scaleY);
- },
-
- /**
- * Get the cropped area position and size data (base on the original image)
- *
- * @param {Boolean} isRounded (optional)
- * @return {Object} data
- */
- getData: function (isRounded) {
- var options = this.options;
- var image = this.image;
- var canvas = this.canvas;
- var cropBox = this.cropBox;
- var ratio;
- var data;
-
- if (this.isBuilt && this.isCropped) {
- data = {
- x: cropBox.left - canvas.left,
- y: cropBox.top - canvas.top,
- width: cropBox.width,
- height: cropBox.height
- };
-
- ratio = image.width / image.naturalWidth;
-
- $.each(data, function (i, n) {
- n = n / ratio;
- data[i] = isRounded ? round(n) : n;
- });
-
- } else {
- data = {
- x: 0,
- y: 0,
- width: 0,
- height: 0
- };
- }
-
- if (options.rotatable) {
- data.rotate = image.rotate || 0;
- }
-
- if (options.scalable) {
- data.scaleX = image.scaleX || 1;
- data.scaleY = image.scaleY || 1;
- }
-
- return data;
- },
-
- /**
- * Set the cropped area position and size with new data
- *
- * @param {Object} data
- */
- setData: function (data) {
- var options = this.options;
- var image = this.image;
- var canvas = this.canvas;
- var cropBoxData = {};
- var isRotated;
- var isScaled;
- var ratio;
-
- if ($.isFunction(data)) {
- data = data.call(this.element);
- }
-
- if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
- if (options.rotatable) {
- if (isNumber(data.rotate) && data.rotate !== image.rotate) {
- image.rotate = data.rotate;
- this.isRotated = isRotated = true;
- }
- }
-
- if (options.scalable) {
- if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) {
- image.scaleX = data.scaleX;
- isScaled = true;
- }
-
- if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) {
- image.scaleY = data.scaleY;
- isScaled = true;
- }
- }
-
- if (isRotated) {
- this.renderCanvas();
- } else if (isScaled) {
- this.renderImage();
- }
-
- ratio = image.width / image.naturalWidth;
-
- if (isNumber(data.x)) {
- cropBoxData.left = data.x * ratio + canvas.left;
- }
-
- if (isNumber(data.y)) {
- cropBoxData.top = data.y * ratio + canvas.top;
- }
-
- if (isNumber(data.width)) {
- cropBoxData.width = data.width * ratio;
- }
-
- if (isNumber(data.height)) {
- cropBoxData.height = data.height * ratio;
- }
-
- this.setCropBoxData(cropBoxData);
- }
- },
-
- /**
- * Get the container size data
- *
- * @return {Object} data
- */
- getContainerData: function () {
- return this.isBuilt ? this.container : {};
- },
-
- /**
- * Get the image position and size data
- *
- * @return {Object} data
- */
- getImageData: function () {
- return this.isLoaded ? this.image : {};
- },
-
- /**
- * Get the canvas position and size data
- *
- * @return {Object} data
- */
- getCanvasData: function () {
- var canvas = this.canvas;
- var data = {};
-
- if (this.isBuilt) {
- $.each([
- 'left',
- 'top',
- 'width',
- 'height',
- 'naturalWidth',
- 'naturalHeight'
- ], function (i, n) {
- data[n] = canvas[n];
- });
- }
-
- return data;
- },
-
- /**
- * Set the canvas position and size with new data
- *
- * @param {Object} data
- */
- setCanvasData: function (data) {
- var canvas = this.canvas;
- var aspectRatio = canvas.aspectRatio;
-
- if ($.isFunction(data)) {
- data = data.call(this.$element);
- }
-
- if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
- if (isNumber(data.left)) {
- canvas.left = data.left;
- }
-
- if (isNumber(data.top)) {
- canvas.top = data.top;
- }
-
- if (isNumber(data.width)) {
- canvas.width = data.width;
- canvas.height = data.width / aspectRatio;
- } else if (isNumber(data.height)) {
- canvas.height = data.height;
- canvas.width = data.height * aspectRatio;
- }
-
- this.renderCanvas(true);
- }
- },
-
- /**
- * Get the crop box position and size data
- *
- * @return {Object} data
- */
- getCropBoxData: function () {
- var cropBox = this.cropBox;
- var data;
-
- if (this.isBuilt && this.isCropped) {
- data = {
- left: cropBox.left,
- top: cropBox.top,
- width: cropBox.width,
- height: cropBox.height
- };
- }
-
- return data || {};
- },
-
- /**
- * Set the crop box position and size with new data
- *
- * @param {Object} data
- */
- setCropBoxData: function (data) {
- var cropBox = this.cropBox;
- var aspectRatio = this.options.aspectRatio;
- var isWidthChanged;
- var isHeightChanged;
-
- if ($.isFunction(data)) {
- data = data.call(this.$element);
- }
-
- if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) {
-
- if (isNumber(data.left)) {
- cropBox.left = data.left;
- }
-
- if (isNumber(data.top)) {
- cropBox.top = data.top;
- }
-
- if (isNumber(data.width)) {
- isWidthChanged = true;
- cropBox.width = data.width;
- }
-
- if (isNumber(data.height)) {
- isHeightChanged = true;
- cropBox.height = data.height;
- }
-
- if (aspectRatio) {
- if (isWidthChanged) {
- cropBox.height = cropBox.width / aspectRatio;
- } else if (isHeightChanged) {
- cropBox.width = cropBox.height * aspectRatio;
- }
- }
-
- this.renderCropBox();
- }
- },
-
- /**
- * Get a canvas drawn the cropped image
- *
- * @param {Object} options (optional)
- * @return {HTMLCanvasElement} canvas
- */
- getCroppedCanvas: function (options) {
- var originalWidth;
- var originalHeight;
- var canvasWidth;
- var canvasHeight;
- var scaledWidth;
- var scaledHeight;
- var scaledRatio;
- var aspectRatio;
- var canvas;
- var context;
- var data;
-
- if (!this.isBuilt || !this.isCropped || !SUPPORT_CANVAS) {
- return;
- }
-
- if (!$.isPlainObject(options)) {
- options = {};
- }
-
- data = this.getData();
- originalWidth = data.width;
- originalHeight = data.height;
- aspectRatio = originalWidth / originalHeight;
-
- if ($.isPlainObject(options)) {
- scaledWidth = options.width;
- scaledHeight = options.height;
-
- if (scaledWidth) {
- scaledHeight = scaledWidth / aspectRatio;
- scaledRatio = scaledWidth / originalWidth;
- } else if (scaledHeight) {
- scaledWidth = scaledHeight * aspectRatio;
- scaledRatio = scaledHeight / originalHeight;
- }
- }
-
- // The canvas element will use `Math.floor` on a float number, so floor first
- canvasWidth = floor(scaledWidth || originalWidth);
- canvasHeight = floor(scaledHeight || originalHeight);
-
- canvas = $('<canvas>')[0];
- canvas.width = canvasWidth;
- canvas.height = canvasHeight;
- context = canvas.getContext('2d');
-
- if (options.fillColor) {
- context.fillStyle = options.fillColor;
- context.fillRect(0, 0, canvasWidth, canvasHeight);
- }
-
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
- context.drawImage.apply(context, (function () {
- var source = getSourceCanvas(this.$clone[0], this.image);
- var sourceWidth = source.width;
- var sourceHeight = source.height;
- var args = [source];
-
- // Source canvas
- var srcX = data.x;
- var srcY = data.y;
- var srcWidth;
- var srcHeight;
-
- // Destination canvas
- var dstX;
- var dstY;
- var dstWidth;
- var dstHeight;
-
- if (srcX <= -originalWidth || srcX > sourceWidth) {
- srcX = srcWidth = dstX = dstWidth = 0;
- } else if (srcX <= 0) {
- dstX = -srcX;
- srcX = 0;
- srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX);
- } else if (srcX <= sourceWidth) {
- dstX = 0;
- srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX);
- }
-
- if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) {
- srcY = srcHeight = dstY = dstHeight = 0;
- } else if (srcY <= 0) {
- dstY = -srcY;
- srcY = 0;
- srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY);
- } else if (srcY <= sourceHeight) {
- dstY = 0;
- srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY);
- }
-
- // All the numerical parameters should be integer for `drawImage` (#476)
- args.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight));
-
- // Scale destination sizes
- if (scaledRatio) {
- dstX *= scaledRatio;
- dstY *= scaledRatio;
- dstWidth *= scaledRatio;
- dstHeight *= scaledRatio;
- }
-
- // Avoid "IndexSizeError" in IE and Firefox
- if (dstWidth > 0 && dstHeight > 0) {
- args.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
- }
-
- return args;
- }).call(this));
-
- return canvas;
- },
-
- /**
- * Change the aspect ratio of the crop box
- *
- * @param {Number} aspectRatio
- */
- setAspectRatio: function (aspectRatio) {
- var options = this.options;
-
- if (!this.isDisabled && !isUndefined(aspectRatio)) {
-
- // 0 -> NaN
- options.aspectRatio = max(0, aspectRatio) || NaN;
-
- if (this.isBuilt) {
- this.initCropBox();
-
- if (this.isCropped) {
- this.renderCropBox();
- }
- }
- }
- },
-
- /**
- * Change the drag mode
- *
- * @param {String} mode (optional)
- */
- setDragMode: function (mode) {
- var options = this.options;
- var croppable;
- var movable;
-
- if (this.isLoaded && !this.isDisabled) {
- croppable = mode === ACTION_CROP;
- movable = options.movable && mode === ACTION_MOVE;
- mode = (croppable || movable) ? mode : ACTION_NONE;
-
- this.$dragBox.
- data(DATA_ACTION, mode).
- toggleClass(CLASS_CROP, croppable).
- toggleClass(CLASS_MOVE, movable);
-
- if (!options.cropBoxMovable) {
-
- // Sync drag mode to crop box when it is not movable(#300)
- this.$face.
- data(DATA_ACTION, mode).
- toggleClass(CLASS_CROP, croppable).
- toggleClass(CLASS_MOVE, movable);
- }
- }
- }
- };
-
- Cropper.DEFAULTS = {
-
- // Define the view mode of the cropper
- viewMode: 0, // 0, 1, 2, 3
-
- // Define the dragging mode of the cropper
- dragMode: 'crop', // 'crop', 'move' or 'none'
-
- // Define the aspect ratio of the crop box
- aspectRatio: NaN,
-
- // An object with the previous cropping result data
- data: null,
-
- // A jQuery selector for adding extra containers to preview
- preview: '',
-
- // Re-render the cropper when resize the window
- responsive: true,
-
- // Restore the cropped area after resize the window
- restore: true,
-
- // Check if the current image is a cross-origin image
- checkCrossOrigin: true,
-
- // Check the current image's Exif Orientation information
- checkOrientation: true,
-
- // Show the black modal
- modal: true,
-
- // Show the dashed lines for guiding
- guides: true,
-
- // Show the center indicator for guiding
- center: true,
-
- // Show the white modal to highlight the crop box
- highlight: true,
-
- // Show the grid background
- background: true,
-
- // Enable to crop the image automatically when initialize
- autoCrop: true,
-
- // Define the percentage of automatic cropping area when initializes
- autoCropArea: 0.8,
-
- // Enable to move the image
- movable: true,
-
- // Enable to rotate the image
- rotatable: true,
-
- // Enable to scale the image
- scalable: true,
-
- // Enable to zoom the image
- zoomable: true,
-
- // Enable to zoom the image by dragging touch
- zoomOnTouch: true,
-
- // Enable to zoom the image by wheeling mouse
- zoomOnWheel: true,
-
- // Define zoom ratio when zoom the image by wheeling mouse
- wheelZoomRatio: 0.1,
-
- // Enable to move the crop box
- cropBoxMovable: true,
-
- // Enable to resize the crop box
- cropBoxResizable: true,
-
- // Toggle drag mode between "crop" and "move" when click twice on the cropper
- toggleDragModeOnDblclick: true,
-
- // Size limitation
- minCanvasWidth: 0,
- minCanvasHeight: 0,
- minCropBoxWidth: 0,
- minCropBoxHeight: 0,
- minContainerWidth: 200,
- minContainerHeight: 100,
-
- // Shortcuts of events
- build: null,
- built: null,
- cropstart: null,
- cropmove: null,
- cropend: null,
- crop: null,
- zoom: null
- };
-
- Cropper.setDefaults = function (options) {
- $.extend(Cropper.DEFAULTS, options);
- };
-
- Cropper.TEMPLATE = (
- '<div class="cropper-container">' +
- '<div class="cropper-wrap-box">' +
- '<div class="cropper-canvas"></div>' +
- '</div>' +
- '<div class="cropper-drag-box"></div>' +
- '<div class="cropper-crop-box">' +
- '<span class="cropper-view-box"></span>' +
- '<span class="cropper-dashed dashed-h"></span>' +
- '<span class="cropper-dashed dashed-v"></span>' +
- '<span class="cropper-center"></span>' +
- '<span class="cropper-face"></span>' +
- '<span class="cropper-line line-e" data-action="e"></span>' +
- '<span class="cropper-line line-n" data-action="n"></span>' +
- '<span class="cropper-line line-w" data-action="w"></span>' +
- '<span class="cropper-line line-s" data-action="s"></span>' +
- '<span class="cropper-point point-e" data-action="e"></span>' +
- '<span class="cropper-point point-n" data-action="n"></span>' +
- '<span class="cropper-point point-w" data-action="w"></span>' +
- '<span class="cropper-point point-s" data-action="s"></span>' +
- '<span class="cropper-point point-ne" data-action="ne"></span>' +
- '<span class="cropper-point point-nw" data-action="nw"></span>' +
- '<span class="cropper-point point-sw" data-action="sw"></span>' +
- '<span class="cropper-point point-se" data-action="se"></span>' +
- '</div>' +
- '</div>'
- );
-
- // Save the other cropper
- Cropper.other = $.fn.cropper;
-
- // Register as jQuery plugin
- $.fn.cropper = function (option) {
- var args = toArray(arguments, 1);
- var result;
-
- this.each(function () {
- var $this = $(this);
- var data = $this.data(NAMESPACE);
- var options;
- var fn;
-
- if (!data) {
- if (/destroy/.test(option)) {
- return;
- }
-
- options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
- $this.data(NAMESPACE, (data = new Cropper(this, options)));
- }
-
- if (typeof option === 'string' && $.isFunction(fn = data[option])) {
- result = fn.apply(data, args);
- }
- });
-
- return isUndefined(result) ? this : result;
- };
-
- $.fn.cropper.Constructor = Cropper;
- $.fn.cropper.setDefaults = Cropper.setDefaults;
-
- // No conflict
- $.fn.cropper.noConflict = function () {
- $.fn.cropper = Cropper.other;
- return this;
- };
-
-});
diff --git a/vendor/assets/stylesheets/cropper.css b/vendor/assets/stylesheets/cropper.css
deleted file mode 100755
index 41ee4bd546c..00000000000
--- a/vendor/assets/stylesheets/cropper.css
+++ /dev/null
@@ -1,379 +0,0 @@
-/*!
- * Cropper v2.2.5
- * https://github.com/fengyuanchen/cropper
- *
- * Copyright (c) 2014-2016 Fengyuan Chen and contributors
- * Released under the MIT license
- *
- * Date: 2016-01-18T05:42:29.639Z
- */
-.cropper-container {
- font-size: 0;
- line-height: 0;
-
- position: relative;
-
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-
- direction: ltr !important;
- -ms-touch-action: none;
- touch-action: none;
- -webkit-tap-highlight-color: transparent;
- -webkit-touch-callout: none;
-}
-
-.cropper-container img {
- display: block;
-
- width: 100%;
- min-width: 0 !important;
- max-width: none !important;
- height: 100%;
- min-height: 0 !important;
- max-height: none !important;
-
- image-orientation: 0deg !important;
-}
-
-.cropper-wrap-box,
-.cropper-canvas,
-.cropper-drag-box,
-.cropper-crop-box,
-.cropper-modal {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-}
-
-.cropper-wrap-box {
- overflow: hidden;
-}
-
-.cropper-drag-box {
- opacity: 0;
- background-color: #fff;
-
- filter: alpha(opacity=0);
-}
-
-.cropper-modal {
- opacity: .5;
- background-color: #000;
-
- filter: alpha(opacity=50);
-}
-
-.cropper-view-box {
- display: block;
- overflow: hidden;
-
- width: 100%;
- height: 100%;
-
- outline: 1px solid #39f;
- outline-color: rgba(51, 153, 255, .75);
-}
-
-.cropper-dashed {
- position: absolute;
-
- display: block;
-
- opacity: .5;
- border: 0 dashed #eee;
-
- filter: alpha(opacity=50);
-}
-
-.cropper-dashed.dashed-h {
- top: 33.33333%;
- left: 0;
-
- width: 100%;
- height: 33.33333%;
-
- border-top-width: 1px;
- border-bottom-width: 1px;
-}
-
-.cropper-dashed.dashed-v {
- top: 0;
- left: 33.33333%;
-
- width: 33.33333%;
- height: 100%;
-
- border-right-width: 1px;
- border-left-width: 1px;
-}
-
-.cropper-center {
- position: absolute;
- top: 50%;
- left: 50%;
-
- display: block;
-
- width: 0;
- height: 0;
-
- opacity: .75;
-
- filter: alpha(opacity=75);
-}
-
-.cropper-center:before,
-.cropper-center:after {
- position: absolute;
-
- display: block;
-
- content: ' ';
-
- background-color: #eee;
-}
-
-.cropper-center:before {
- top: 0;
- left: -3px;
-
- width: 7px;
- height: 1px;
-}
-
-.cropper-center:after {
- top: -3px;
- left: 0;
-
- width: 1px;
- height: 7px;
-}
-
-.cropper-face,
-.cropper-line,
-.cropper-point {
- position: absolute;
-
- display: block;
-
- width: 100%;
- height: 100%;
-
- opacity: .1;
-
- filter: alpha(opacity=10);
-}
-
-.cropper-face {
- top: 0;
- left: 0;
-
- background-color: #fff;
-}
-
-.cropper-line {
- background-color: #39f;
-}
-
-.cropper-line.line-e {
- top: 0;
- right: -3px;
-
- width: 5px;
-
- cursor: e-resize;
-}
-
-.cropper-line.line-n {
- top: -3px;
- left: 0;
-
- height: 5px;
-
- cursor: n-resize;
-}
-
-.cropper-line.line-w {
- top: 0;
- left: -3px;
-
- width: 5px;
-
- cursor: w-resize;
-}
-
-.cropper-line.line-s {
- bottom: -3px;
- left: 0;
-
- height: 5px;
-
- cursor: s-resize;
-}
-
-.cropper-point {
- width: 5px;
- height: 5px;
-
- opacity: .75;
- background-color: #39f;
-
- filter: alpha(opacity=75);
-}
-
-.cropper-point.point-e {
- top: 50%;
- right: -3px;
-
- margin-top: -3px;
-
- cursor: e-resize;
-}
-
-.cropper-point.point-n {
- top: -3px;
- left: 50%;
-
- margin-left: -3px;
-
- cursor: n-resize;
-}
-
-.cropper-point.point-w {
- top: 50%;
- left: -3px;
-
- margin-top: -3px;
-
- cursor: w-resize;
-}
-
-.cropper-point.point-s {
- bottom: -3px;
- left: 50%;
-
- margin-left: -3px;
-
- cursor: s-resize;
-}
-
-.cropper-point.point-ne {
- top: -3px;
- right: -3px;
-
- cursor: ne-resize;
-}
-
-.cropper-point.point-nw {
- top: -3px;
- left: -3px;
-
- cursor: nw-resize;
-}
-
-.cropper-point.point-sw {
- bottom: -3px;
- left: -3px;
-
- cursor: sw-resize;
-}
-
-.cropper-point.point-se {
- right: -3px;
- bottom: -3px;
-
- width: 20px;
- height: 20px;
-
- cursor: se-resize;
-
- opacity: 1;
-
- filter: alpha(opacity=100);
-}
-
-.cropper-point.point-se:before {
- position: absolute;
- right: -50%;
- bottom: -50%;
-
- display: block;
-
- width: 200%;
- height: 200%;
-
- content: ' ';
-
- opacity: 0;
- background-color: #39f;
-
- filter: alpha(opacity=0);
-}
-
-@media (min-width: 768px) {
- .cropper-point.point-se {
- width: 15px;
- height: 15px;
- }
-}
-
-@media (min-width: 992px) {
- .cropper-point.point-se {
- width: 10px;
- height: 10px;
- }
-}
-
-@media (min-width: 1200px) {
- .cropper-point.point-se {
- width: 5px;
- height: 5px;
-
- opacity: .75;
-
- filter: alpha(opacity=75);
- }
-}
-
-.cropper-invisible {
- opacity: 0;
-
- filter: alpha(opacity=0);
-}
-
-.cropper-bg {
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
-}
-
-.cropper-hide {
- position: absolute;
-
- display: block;
-
- width: 0;
- height: 0;
-}
-
-.cropper-hidden {
- display: none !important;
-}
-
-.cropper-move {
- cursor: move;
-}
-
-.cropper-crop {
- cursor: crosshair;
-}
-
-.cropper-disabled .cropper-drag-box,
-.cropper-disabled .cropper-face,
-.cropper-disabled .cropper-line,
-.cropper-disabled .cropper-point {
- cursor: not-allowed;
-}