summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAchilleas Pipinellis <axilleas@axilleas.me>2016-03-17 15:56:44 +0200
committerAchilleas Pipinellis <axilleas@axilleas.me>2016-03-17 15:56:44 +0200
commit5b1db58c852d6b36b91a61ec66e2f3cb5187f3a2 (patch)
tree17d90632a853b6ff92a04a44ae156557da813a49
parenta6f5304280d95c496b7669d1553cc6c246f11f94 (diff)
parent4171933c0963696626c879c2d05afa1594a71d99 (diff)
downloadgitlab-ce-5b1db58c852d6b36b91a61ec66e2f3cb5187f3a2.tar.gz
Merge branch 'master' into rm_duplicate_cache_ci_docs
-rw-r--r--.csscomb.json16
-rw-r--r--.gitlab-ci.yml15
-rw-r--r--.scss-lint.yml158
-rw-r--r--CHANGELOG20
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Gemfile11
-rw-r--r--Gemfile.lock22
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/application.js.coffee29
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee2
-rw-r--r--app/assets/javascripts/profile.js.coffee48
-rw-r--r--app/assets/javascripts/sidebar.js.coffee18
-rw-r--r--app/assets/javascripts/subscription.js.coffee34
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss6
-rw-r--r--app/assets/stylesheets/framework/buttons.scss15
-rw-r--r--app/assets/stylesheets/framework/calendar.scss8
-rw-r--r--app/assets/stylesheets/framework/callout.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss18
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss9
-rw-r--r--app/assets/stylesheets/framework/files.scss6
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss28
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/jquery.scss18
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss14
-rw-r--r--app/assets/stylesheets/framework/mobile.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss8
-rw-r--r--app/assets/stylesheets/framework/selects.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss24
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss101
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss18
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss2
-rw-r--r--app/assets/stylesheets/highlight/white.scss80
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss42
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/emojis.scss164
-rw-r--r--app/assets/stylesheets/pages/events.scss8
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/import.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss12
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss14
-rw-r--r--app/assets/stylesheets/pages/profile.scss56
-rw-r--r--app/assets/stylesheets/pages/projects.scss18
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/assets/stylesheets/pages/xterm.scss52
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb17
-rw-r--r--app/controllers/oauth/applications_controller.rb24
-rw-r--r--app/controllers/profiles_controller.rb10
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rw-r--r--app/controllers/projects/labels_controller.rb9
-rw-r--r--app/controllers/projects/merge_requests_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb13
-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/labels_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb28
-rw-r--r--app/mailers/emails/merge_requests.rb49
-rw-r--r--app/mailers/emails/profile.rb5
-rw-r--r--app/models/abuse_report.rb4
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/commit.rb34
-rw-r--r--app/models/commit_status.rb18
-rw-r--r--app/models/concerns/issuable.rb22
-rw-r--r--app/models/concerns/subscribable.rb44
-rw-r--r--app/models/key.rb3
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_wiki.rb8
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/wiki_page.rb2
-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/delete_user_service.rb24
-rw-r--r--app/services/destroy_group_service.rb4
-rw-r--r--app/services/git_push_service.rb11
-rw-r--r--app/services/issuable_base_service.rb17
-rw-r--r--app/services/issues/update_service.rb9
-rw-r--r--app/services/merge_requests/update_service.rb13
-rw-r--r--app/services/notification_service.rb70
-rw-r--r--app/services/projects/housekeeping_service.rb27
-rw-r--r--app/uploaders/avatar_uploader.rb11
-rw-r--r--app/views/admin/builds/_build.html.haml21
-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/_projects.html.haml13
-rw-r--r--app/views/groups/_shared_projects.html.haml19
-rw-r--r--app/views/groups/show.html.haml33
-rw-r--r--app/views/layouts/nav/_profile.html.haml2
-rw-r--r--app/views/layouts/notify.html.haml13
-rw-r--r--app/views/notify/_reassigned_issuable_email.text.erb2
-rw-r--r--app/views/notify/_relabeled_issuable_email.html.haml3
-rw-r--r--app/views/notify/_relabeled_issuable_email.text.erb3
-rw-r--r--app/views/notify/relabeled_issue_email.html.haml1
-rw-r--r--app/views/notify/relabeled_issue_email.text.erb1
-rw-r--r--app/views/notify/relabeled_merge_request_email.html.haml1
-rw-r--r--app/views/notify/relabeled_merge_request_email.text.erb1
-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/labels/_label.html.haml10
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/workers/delete_user_worker.rb10
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/gollum.rb13
-rw-r--r--config/routes.rb6
-rw-r--r--db/migrate/20160314143402_projects_add_pushes_since_gc.rb5
-rw-r--r--db/migrate/20160316123110_ci_runners_token_index.rb13
-rw-r--r--db/schema.rb10
-rw-r--r--doc/api/builds.md7
-rw-r--r--doc/ci/deployment/README.md2
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md2
-rw-r--r--doc/ci/yaml/README.md296
-rw-r--r--doc/development/scss_styleguide.md194
-rw-r--r--doc/install/installation.md24
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/integration/auth0.md89
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--features/profile/profile.feature3
-rw-r--r--features/project/labels.feature15
-rw-r--r--features/project/merge_requests.feature9
-rw-r--r--features/steps/profile/profile.rb30
-rw-r--r--features/steps/project/issues/award_emoji.rb5
-rw-r--r--features/steps/project/labels.rb34
-rw-r--r--features/steps/project/merge_requests.rb20
-rw-r--r--features/steps/search.rb2
-rw-r--r--features/steps/shared/builds.rb2
-rw-r--r--features/steps/shared/issuable.rb4
-rw-r--r--features/support/capybara.rb2
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/banzai/filter/sanitization_filter.rb9
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb27
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/tasks/gemojione.rake1
-rw-r--r--lib/tasks/gitlab/check.rake2
-rw-r--r--lib/tasks/scss-lint.rake10
-rw-r--r--spec/controllers/namespaces_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/factories/labels.rb2
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/users.rb7
-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/diff/parser_spec.rb5
-rw-r--r--spec/mailers/emails/profile_spec.rb6
-rw-r--r--spec/mailers/notify_spec.rb56
-rw-r--r--spec/mailers/shared/notify.rb6
-rw-r--r--spec/models/abuse_report_spec.rb16
-rw-r--r--spec/models/build_spec.rb66
-rw-r--r--spec/models/ci/commit_spec.rb44
-rw-r--r--spec/models/concerns/issuable_spec.rb42
-rw-r--r--spec/models/concerns/subscribable_spec.rb57
-rw-r--r--spec/models/project_spec.rb6
-rw-r--r--spec/models/repository_spec.rb18
-rw-r--r--spec/models/user_spec.rb26
-rw-r--r--spec/services/delete_user_service_spec.rb58
-rw-r--r--spec/services/git_push_service_spec.rb39
-rw-r--r--spec/services/issues/update_service_spec.rb45
-rw-r--r--spec/services/merge_requests/update_service_spec.rb45
-rw-r--r--spec/services/notification_service_spec.rb88
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb48
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/email_helpers.rb13
-rw-r--r--spec/support/wait_for_ajax.rb2
-rw-r--r--spec/workers/delete_user_worker_spec.rb20
-rwxr-xr-xvendor/assets/javascripts/cropper.js2972
-rwxr-xr-xvendor/assets/stylesheets/cropper.css379
212 files changed, 2860 insertions, 4709 deletions
diff --git a/.csscomb.json b/.csscomb.json
new file mode 100644
index 00000000000..e353e6a63d0
--- /dev/null
+++ b/.csscomb.json
@@ -0,0 +1,16 @@
+{
+ "always-semicolon": true,
+ "color-case": "lower",
+ "block-indent": " ",
+ "color-shorthand": true,
+ "element-case": "lower",
+ "space-before-colon": "",
+ "space-after-colon": " ",
+ "space-before-combinator": " ",
+ "space-after-combinator": " ",
+ "space-between-declarations": "\n",
+ "space-before-opening-brace": " ",
+ "space-after-opening-brace": "\n",
+ "space-before-closing-brace": "\n",
+ "unitless-zero": true
+}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bd013d50faa..2ad63548d78 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -122,6 +122,14 @@ rubocop:
- ruby
- mysql
+scss-lint:
+ stage: test
+ script:
+ - bundle exec rake scss_lint
+ tags:
+ - ruby
+ allow_failure: true
+
brakeman:
stage: test
script:
@@ -148,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
@@ -162,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/.scss-lint.yml b/.scss-lint.yml
new file mode 100644
index 00000000000..e350b2073c3
--- /dev/null
+++ b/.scss-lint.yml
@@ -0,0 +1,158 @@
+# Linter Documentation:
+# https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
+
+scss_files: 'app/assets/stylesheets/**/*.scss'
+
+exclude:
+ - 'app/assets/stylesheets/pages/emojis.scss'
+
+linters:
+ BangFormat:
+ enabled: false
+
+ BorderZero:
+ enabled: false
+
+ ColorKeyword:
+ enabled: false
+
+ ColorVariable:
+ enabled: false
+
+ Comment:
+ enabled: false
+
+ DeclarationOrder:
+ enabled: false
+
+ # `scss-lint:disable` control comments should be preceded by a comment
+ # explaining why these linters are being disabled for this file.
+ # See https://github.com/brigade/scss-lint#disabling-linters-via-source for
+ # more information.
+ DisableLinterReason:
+ enabled: true
+
+ DuplicateProperty:
+ enabled: false
+
+ EmptyLineBetweenBlocks:
+ enabled: false
+
+ EmptyRule:
+ enabled: false
+
+ FinalNewline:
+ enabled: false
+
+ # HEX colors should use three-character values where possible.
+ HexLength:
+ enabled: true
+
+ # HEX color values should use lower-case colors to differentiate between
+ # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
+ HexNotation:
+ enabled: true
+
+ IdSelector:
+ enabled: false
+
+ ImportPath:
+ enabled: false
+
+ ImportantRule:
+ enabled: false
+
+ # Indentation should always be done in increments of 2 spaces.
+ Indentation:
+ enabled: true
+ width: 2
+
+ LeadingZero:
+ enabled: false
+
+ MergeableSelector:
+ enabled: false
+
+ NameFormat:
+ enabled: false
+
+ NestingDepth:
+ enabled: false
+
+ PlaceholderInExtend:
+ enabled: false
+
+ PropertySortOrder:
+ enabled: false
+
+ PropertySpelling:
+ enabled: false
+
+ PseudoElement:
+ enabled: false
+
+ QualifyingElement:
+ enabled: false
+
+ SelectorDepth:
+ enabled: false
+
+ # Selectors should always use hyphenated-lowercase, rather than camelCase or
+ # snake_case.
+ SelectorFormat:
+ enabled: true
+ convention: hyphenated_lowercase
+
+ # Prefer the shortest shorthand form possible for properties that support it.
+ Shorthand:
+ enabled: true
+
+ # Each property should have its own line, except in the special case of
+ # single line rulesets.
+ SingleLinePerProperty:
+ enabled: true
+ allow_single_line_rule_sets: true
+
+ SingleLinePerSelector:
+ enabled: false
+
+ SpaceAfterComma:
+ enabled: false
+
+ # Properties should be formatted with a single space separating the colon
+ # from the property's value.
+ SpaceAfterPropertyColon:
+ enabled: true
+
+ # Properties should be formatted with no space between the name and the
+ # colon.
+ SpaceAfterPropertyName:
+ enabled: true
+
+ SpaceAroundOperator:
+ enabled: false
+
+ SpaceBeforeBrace:
+ enabled: false
+
+ StringQuotes:
+ enabled: false
+
+ TrailingSemicolon:
+ enabled: false
+
+ TrailingWhitespace:
+ enabled: false
+
+ UnnecessaryMantissa:
+ enabled: false
+
+ UnnecessaryParentReference:
+ enabled: false
+
+ VendorPrefix:
+ enabled: false
+
+ # Omit length units on zero values, e.g. `0px` vs. `0`.
+ ZeroUnit:
+ enabled: true
diff --git a/CHANGELOG b/CHANGELOG
index b120810ebd8..638a4f1d3fe 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,29 +1,40 @@
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)
+ - Add support for wiki with UTF-8 page names (Hiroyuki Sato)
+ - Fix wiki search results point to raw source (Hiroyuki Sato)
- 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)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
+ - Added omniauth-auth0 Gem (Daniel Carraro)
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- 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
+ - Allow user subscription to a label: get notified for issues/merge requests related to that label (Timothy Andrew)
- 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
@@ -31,6 +42,13 @@ v 8.6.0 (unreleased)
- 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
+ - User deletion is now done in the background so the request can not time out
+
+v 8.5.7
+ - Bump Git version requirement to 2.7.3
+
+v 8.5.6
+ - Obtain a lease before querying LDAP
v 8.5.5
- Ensure removing a project removes associated Todo entries
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 30c97429040..7540fa1afcc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -427,6 +427,7 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
+1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md)
@@ -494,6 +495,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
+[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
diff --git a/Gemfile b/Gemfile
index 1550afb1b56..a3fb6779e9a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.4'
gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.3.1'
+gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
@@ -58,7 +59,9 @@ gem "gitlab_git", '~> 9.0'
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki
-gem 'gollum-lib', '~> 4.1.0'
+# Required manually in config/initializers/gollum.rb to control load order
+gem 'gollum-lib', '~> 4.1.0', require: false
+gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
# Language detection
gem "github-linguist", "~> 4.7.0", require: "linguist"
@@ -77,9 +80,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 +273,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'
@@ -286,6 +286,7 @@ group :development, :test do
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.35.0', require: false
+ gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
gem 'flog', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index d4e28db00d6..7b0dd83da52 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)
@@ -380,6 +381,9 @@ GEM
rouge (~> 1.9)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
+ gollum-rugged_adapter (0.4.2)
+ mime-types (>= 1.15)
+ rugged (~> 0.24.0, >= 0.21.3)
gon (6.0.1)
actionpack (>= 3.0)
json
@@ -468,7 +472,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)
@@ -495,6 +498,8 @@ GEM
omniauth (1.3.1)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
+ omniauth-auth0 (1.4.1)
+ omniauth-oauth2 (~> 1.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -703,7 +708,7 @@ GEM
rubyntlm (0.5.2)
rubypants (0.2.0)
rufus-scheduler (3.1.10)
- rugged (0.24.0b13)
+ rugged (0.24.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -717,6 +722,9 @@ GEM
sawyer (0.6.0)
addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10)
+ scss_lint (0.47.1)
+ rake (>= 0.9, < 11)
+ sass (~> 3.4.15)
sdoc (0.3.20)
json (>= 1.1.3)
rdoc (~> 3.10)
@@ -901,7 +909,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)
@@ -938,6 +946,7 @@ DEPENDENCIES
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
+ gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.0.1)
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
@@ -956,7 +965,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)
@@ -967,6 +975,7 @@ DEPENDENCIES
oauth2 (~> 1.0.0)
octokit (~> 3.8.0)
omniauth (~> 1.3.1)
+ omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
@@ -1008,6 +1017,7 @@ DEPENDENCIES
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
+ scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
diff --git a/README.md b/README.md
index 3ec1d4a776c..208427fcf8c 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
-- Git 1.7.10+
+- Git 2.7.3+
- Redis 2.8+
- MySQL or PostgreSQL
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/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index b94de4c7b5e..4f038477755 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -251,7 +251,7 @@ class GitLabDropdown
# 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/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 7f41616d4e7..084f0e0dc65 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -1,17 +1,21 @@
class @Subscription
- constructor: (url) ->
- $(".subscribe-button").unbind("click").click (event)=>
- btn = $(event.currentTarget)
- action = btn.find("span").text()
- current_status = $(".subscription-status").attr("data-status")
- btn.prop("disabled", true)
-
- $.post url, =>
- btn.prop("disabled", false)
- status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
- $(".subscription-status").attr("data-status", status)
- action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
- btn.find("span").text(action)
- $(".subscription-status>div").toggleClass("hidden")
+ constructor: (container) ->
+ $container = $(container)
+ @url = $container.attr('data-url')
+ @subscribe_button = $container.find('.subscribe-button')
+ @subscription_status = $container.find('.subscription-status')
+ @subscribe_button.unbind('click').click(@toggleSubscription)
-
+ toggleSubscription: (event) =>
+ btn = $(event.currentTarget)
+ action = btn.find('span').text()
+ current_status = @subscription_status.attr('data-status')
+ btn.prop('disabled', true)
+
+ $.post @url, =>
+ btn.prop('disabled', false)
+ status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
+ @subscription_status.attr('data-status', status)
+ action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
+ btn.find('span').text(action)
+ @subscription_status.find('>div').toggleClass('hidden')
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..90c3ce0e84c 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -23,7 +23,7 @@
margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
color: $gl-gray;
@@ -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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8d475137b03..fa115a4bf56 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -37,23 +37,23 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
}
@mixin btn-gray {
@@ -127,7 +127,7 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
&.btn-xs {
margin-right: 3px;
@@ -139,7 +139,6 @@
.caret {
margin-left: 5px;
- color: $gray-darkest;
}
}
@@ -170,7 +169,7 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 580012abd77..e3192823a1a 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -33,19 +33,19 @@
}
.q2 {
- fill: #ACD5F2 !important;
+ fill: #acd5f2 !important;
}
.q3 {
- fill: #7FA8D1 !important;
+ fill: #7fa8d1 !important;
}
.q4 {
- fill: #49729B !important;
+ fill: #49729b !important;
}
.q5 {
- fill: #254E77 !important;
+ fill: #254e77 !important;
}
.domain-background {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 20a9bfb9816..da7bab74a32 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -39,6 +39,6 @@
}
.bs-callout-success {
background-color: #dff0d8;
- border-color: #5cA64d;
+ border-color: #5ca64d;
color: #3c763d;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ff551f151f1..180926b3b97 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,6 +1,6 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #BBB }
+.clgray { color: #bbb }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
@@ -51,7 +51,7 @@ pre {
}
&.well-pre {
- border: 1px solid #EEE;
+ border: 1px solid #eee;
background: #f9f9f9;
border-radius: 0;
color: #555;
@@ -103,7 +103,7 @@ span.update-author {
}
.user-mention {
- color: #2FA0BB;
+ color: #2fa0bb;
font-weight: bold;
}
@@ -187,9 +187,9 @@ li.note {
.error-message {
padding: 10px;
- background: #C67;
+ background: #c67;
margin: 0;
- color: #FFF;
+ color: #fff;
a {
color: #fff;
@@ -200,7 +200,7 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #C67;
+ background: #c67;
color: #fff;
font-weight: bold;
a {
@@ -271,7 +271,7 @@ img.emoji {
table {
td.permission-x {
- background: #D9EDF7 !important;
+ background: #d9edf7 !important;
text-align: center;
}
}
@@ -280,7 +280,7 @@ table {
float: left;
text-align: center;
font-size: 32px;
- color: #AAA;
+ color: #aaa;
width: 60px;
}
@@ -347,7 +347,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #EEE !important;
+ border-color: #eee !important;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5b647fc6176..3197ea84460 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -4,7 +4,7 @@
height: 0;
margin-left: 2px;
vertical-align: middle;
- border-top: $caret-width-base dashed $dropdown-caret-color;
+ border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
}
@@ -161,9 +161,8 @@
.dropdown-menu-user-full-name {
display: block;
- margin-bottom: 2px;
font-weight: 600;
- line-height: 1;
+ line-height: 16px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -171,7 +170,7 @@
.dropdown-menu-user-username {
display: block;
- line-height: 1;
+ line-height: 16px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -261,7 +260,7 @@
position: absolute;
top: 10px;
right: 10px;
- color: #C7C7C7;
+ color: #c7c7c7;
font-size: 12px;
pointer-events: none;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index b034a4882c1..646e2610831 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -30,7 +30,7 @@
right: 15px;
.btn {
- padding: 0px 10px;
+ padding: 0 10px;
font-size: 13px;
line-height: 28px;
}
@@ -84,7 +84,7 @@
&.blob-no-preview {
background: #eee;
- text-shadow: 0 1px 2px #FFF;
+ text-shadow: 0 1px 2px #fff;
padding: 100px 0;
}
@@ -124,7 +124,7 @@
}
td.line-numbers {
float: none;
- border-left: 1px solid #DDD;
+ border-left: 1px solid #ddd;
}
td.lines {
padding: 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 6c08005812e..4cb4129b71b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -41,8 +41,8 @@ input[type='search'].search-input {
}
input[type='text'].danger {
- background: #F2DEDE!important;
- border-color: #D66;
+ background: #f2dede!important;
+ border-color: #d66;
text-shadow: 0 1px 1px #fff
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 12cef6f8ea1..2a4cf4fc335 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -23,13 +23,13 @@
&:hover {
background-color: $color-darker;
a {
- color: #FFF;
+ color: #fff;
}
}
}
.collapse-nav a {
- color: #FFF;
+ color: #fff;
background: $color;
}
@@ -42,7 +42,7 @@
&:hover {
background-color: $color-dark;
- color: #FFF;
+ color: #fff;
text-decoration: none;
}
}
@@ -71,7 +71,7 @@
}
&.active a {
- color: #FFF;
+ color: #fff;
background: $color-dark;
&.no-highlight {
@@ -79,42 +79,42 @@
}
i {
- color: #FFF
+ color: #fff
}
}
}
}
}
-$theme-blue: #2980B9;
+$theme-blue: #2980b9;
$theme-charcoal: #333c47;
-$theme-graphite: #888888;
+$theme-graphite: #888;
$theme-gray: #373737;
$theme-green: #019875;
-$theme-violet: #554488;
+$theme-violet: #548;
body {
&.ui_blue {
- @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099);
+ @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
}
&.ui_charcoal {
- @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D);
+ @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d);
}
&.ui_graphite {
- @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666);
+ @include gitlab-theme(#ccc, $theme-graphite, #777, #666);
}
&.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222222);
+ @include gitlab-theme(#979797, $theme-gray, #272727, #222);
}
&.ui_green {
- @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855);
+ @include gitlab-theme(#adc, $theme-green, #018865, #017855);
}
&.ui_violet {
- @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255);
+ @include gitlab-theme(#98c, $theme-violet, #436, #325);
}
} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index e624982c5c9..71a7ecab8ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -7,8 +7,8 @@ header {
&.navbar-empty {
height: 58px;
- background: #FFF;
- border-bottom: 1px solid #EEE;
+ background: #fff;
+ border-bottom: 1px solid #eee;
.center-logo {
margin: 11px 0;
@@ -28,7 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
.container-fluid {
width: 100% !important;
@@ -47,7 +47,7 @@ header {
text-align: center;
&:hover, &:focus, &:active {
- background-color: #FFF;
+ background-color: #fff;
}
}
@@ -59,7 +59,7 @@ header {
right: 2px;
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
&.active {
color: #7f8fa4;
@@ -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;
}
}
@@ -166,7 +162,7 @@ header {
font-size: 18px;
.navbar-nav {
- margin: 0px;
+ margin: 0;
float: none !important;
.visible-xs, .visable-sm {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 12e2f00fe89..7cf4d4fba42 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,8 +1,8 @@
.file-content.code {
border: none;
box-shadow: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
table-layout: fixed;
pre {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 77a00586b26..7f7b7c806e7 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -20,7 +20,7 @@
display: block;
float: left;
margin-right: 10px;
- color: #FFF;
+ color: #fff;
font-size: $gl-font-size;
line-height: 25px;
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 0cdcd923b3c..525ed81b059 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -3,13 +3,13 @@
font-size: $font-size-base;
&.ui-datepicker-inline {
- border: 1px solid #DDD;
+ border: 1px solid #ddd;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #FFF;
- border-color: #DDD;
+ background: #fff;
+ border-color: #ddd;
}
.ui-datepicker-calendar td a {
@@ -19,7 +19,7 @@
}
&.ui-autocomplete {
- border-color: #DDD;
+ border-color: #ddd;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -30,20 +30,20 @@
}
.ui-state-default {
- border: 1px solid #FFF;
- background: #FFF;
+ border: 1px solid #fff;
+ background: #fff;
color: #777;
}
.ui-state-highlight {
- border: 1px solid #EEE;
- background: #EEE;
+ border: 1px solid #eee;
+ background: #eee;
}
.ui-state-active {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #FFF;
+ color: #fff;
}
.ui-state-hover,
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 1d8611b04dc..8328aac4e7a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -71,7 +71,7 @@
}
.md-preview-holder {
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 169px;
padding: 5px;
@@ -80,7 +80,7 @@
.markdown-area {
@include border-radius(0);
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 140px;
max-height: 500px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 368bbfe5355..377bfa174bd 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
@@ -73,17 +67,17 @@
* Base mixin for lists in GitLab
*/
@mixin basic-list {
- margin: 5px 0px;
- padding: 0px;
+ margin: 5px 0;
+ padding: 0;
list-style: none;
> li {
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
display: block;
- margin: 0px;
+ margin: 0;
&:last-child {
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 3bfac2ad9b5..5ea4f9a49db 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -128,12 +128,12 @@
.show-aside {
display: none;
position: fixed;
- right: 0px;
+ right: 0;
top: 30%;
padding: 5px 15px;
- background: #EEE;
+ background: #eee;
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #DDD);
+ @include box-shadow(0 1px 2px #ddd);
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index b2fbc95e043..5f4ce87b085 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -26,7 +26,7 @@
}
&.active a {
- color: #000000;
+ color: #000;
border-bottom: 2px solid #4688f1;
}
@@ -41,7 +41,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
.nav-text {
padding-top: 16px;
@@ -59,7 +59,7 @@
.nav-links {
display: inline-block;
width: 50%;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
@@ -74,7 +74,7 @@
float: right;
text-align: right;
padding: 11px 0;
- margin-bottom: 0px;
+ margin-bottom: 0;
> .dropdown {
margin-right: $gl-padding-top;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 7bf04e4ad74..b3371229d5a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -151,7 +151,7 @@
padding: 2px 25px 2px 5px;
background: #fff image-url('select2.png');
background-repeat: no-repeat;
- background-position: right 0px bottom 6px;
+ background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@include border-radius($border-radius-default);
@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
@@ -229,7 +229,7 @@
.namespace-result {
.namespace-kind {
- color: #AAA;
+ color: #aaa;
font-weight: normal;
}
.namespace-path {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 6b382e4b1b2..be05db58c40 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,7 +16,7 @@
.gitlab-text-container-link {
z-index: 1;
position: absolute;
- left: 0px;
+ left: 0;
}
#logo {
@@ -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;
}
@@ -47,7 +47,7 @@
width: 100%;
.container-fluid {
- background: #FFF;
+ background: #fff;
padding: 0 $gl-padding;
&.container-blank {
@@ -103,7 +103,7 @@
}
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
}
@@ -143,7 +143,7 @@
overflow: hidden;
&.navbar-collapse {
- padding: 0px !important;
+ padding: 0 !important;
}
li {
@@ -182,7 +182,7 @@
.count {
float: right;
background: #eee;
- padding: 0px 8px;
+ padding: 0 8px;
@include border-radius(6px);
}
@@ -194,8 +194,8 @@
}
.sidebar-subnav {
- margin-left: 0px;
- padding-left: 0px;
+ margin-left: 0;
+ padding-left: 0;
li {
list-style: none;
@@ -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/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index ddf76704a53..dd42db1840f 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -95,7 +95,7 @@
}
&.label-inverse {
- background-color: #333333;
+ background-color: #333;
}
}
@@ -138,7 +138,7 @@
}
.btn-clipboard {
- min-width: 0px;
+ min-width: 0;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index b1b8295411b..f63ac033234 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -57,7 +57,7 @@ $component-active-bg: $brand-info;
$input-color: $text-color;
$input-border: #e7e9ed;
-$input-border-focus: #7F8FA4;
+$input-border-focus: #7f8fa4;
$legend-color: $text-color;
@@ -125,8 +125,8 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #F9F9F9;
-$well-border: #EEE;
+$well-bg: #f9f9f9;
+$well-border: #eee;
//== Code
//
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9381cb3281c..949295a1d0c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -27,13 +27,13 @@
line-height: 10px;
color: #555;
vertical-align: middle;
- background-color: #FCFCFC;
+ background-color: #fcfcfc;
border-width: 1px;
border-style: solid;
- border-color: #CCC #CCC #BBB;
+ border-color: #ccc #ccc #bbb;
border-image: none;
border-radius: 3px;
- box-shadow: 0px -1px 0px #BBB inset;
+ box-shadow: 0 -1px 0 #bbb inset;
}
h1 {
@@ -187,7 +187,7 @@ body {
}
.page-title-empty {
- margin-top: 0px;
+ margin-top: 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0261c384a58..5e3546bc6ff 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,8 +1,8 @@
$row-hover: #f4f8fe;
-$gl-text-color: #54565B;
-$gl-text-green: #4A2;
-$gl-text-red: #D12F19;
-$gl-text-orange: #D90;
+$gl-text-color: #54565b;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
@@ -30,24 +30,24 @@ $gl-vert-padding: 6px;
$gl-padding-top:10px;
$gl-avatar-size: 40px;
$secondary-text: #7f8fa4;
-$error-exclamation-point: #E62958;
+$error-exclamation-point: #e62958;
$border-radius-default: 3px;
-$list-title-color: #333333;
-$list-text-color: #555555;
+$list-title-color: #333;
+$list-text-color: #555;
-$btn-transparent-color: #8F8F8F;
+$btn-transparent-color: #8f8f8f;
-$ssh-key-icon-color: #8F8F8F;
+$ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px;
-$provider-btn-group-border: #E5E5E5;
-$provider-btn-not-active-color: #4688F1;
+$provider-btn-group-border: #e5e5e5;
+$provider-btn-not-active-color: #4688f1;
/*
* Color schema
*/
-$white-light: #FFFFFF;
+$white-light: #fff;
$white-normal: #ededed;
$white-dark: #ededed;
@@ -57,51 +57,55 @@ $gray-dark: #ededed;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
-$green-normal: #2FAA60;
-$green-dark: #2CA05B;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
-$blue-light: #2EA8E5;
-$blue-normal: #2D9FD8;
-$blue-dark: #2897CE;
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
-$blue-medium-light: #3498CB;
-$blue-medium: #2F8EBF;
-$blue-medium-dark: #2D86B4;
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
$orange-light: rgba(252, 109, 38, 0.80);
-$orange-normal: #E75E40;
-$orange-dark: #CE5237;
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
-$red-light: #F06559;
-$red-normal: #E52C5A;
-$red-dark: #D22852;
+$red-light: #f06559;
+$red-normal: #e52c5a;
+$red-dark: #d22852;
-$border-white-light: #F1F2F4;
-$border-white-normal: #D6DAE2;
-$border-white-dark: #C6CACF;
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
$border-gray-light: rgba(0, 0, 0, 0.06);
$border-gray-normal: rgba(0, 0, 0, 0.10);;
-$border-gray-dark: #C6CACF;
+$border-gray-dark: #c6cacf;
-$border-green-light: #2FAA60;
-$border-green-normal: #2CA05B;
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
$border-green-dark: #279654;
-$border-blue-light: #2D9FD8;
-$border-blue-normal: #2897CE;
-$border-blue-dark: #258DC1;
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
$border-orange-light: #fc6d26;
-$border-orange-normal: #CE5237;
-$border-orange-dark: #C14E35;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
-$border-red-light: #F24F41;
-$border-red-normal: #D22852;
-$border-red-dark: #CA264F;
+$border-red-light: #f24f41;
+$border-red-normal: #d22852;
+$border-red-dark: #ca264f;
-$help-well-bg: #FAFAFA;
-$help-well-border: #E5E5E5;
+$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;
@@ -139,23 +143,22 @@ $dropdown-border-color: rgba(#000, .1);
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
-$dropdown-caret-color: #54565B;
-$dropdown-title-btn-color: #BFBFBF;
-$dropdown-input-color: #C7C7C7;
+$dropdown-title-btn-color: #bfbfbf;
+$dropdown-input-color: #c7c7c7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
-$dropdown-toggle-border-color: #EAEAEA;
+$dropdown-toggle-border-color: #eaeaea;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
-$dropdown-toggle-icon-color: #C4C4C4;
+$dropdown-toggle-icon-color: #c4c4c4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
-$award-emoji-menu-bg: #FFF;
-$award-emoji-menu-border: #F1F2F4;
-$award-emoji-new-btn-icon-color: #DCDCDC;
+$award-emoji-menu-bg: #fff;
+$award-emoji-menu-border: #f1f2f4;
+$award-emoji-new-btn-icon-color: #dcdcdc;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index c3f27333fad..02e24ec7c4d 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -2,7 +2,7 @@
a.js-zen-enter {
color: $gl-gray;
position: absolute;
- top: 0px;
+ top: 0;
right: 4px;
line-height: 56px;
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index b794da2ce98..47673944896 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -43,12 +43,12 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #373b41 }
.c { color: #969896 } /* Comment */
- .err { color: #cc6666 } /* Error */
+ .err { color: #c66 } /* Error */
.k { color: #b294bb } /* Keyword */
.l { color: #de935f } /* Literal */
.n { color: #c5c8c6 } /* Name */
@@ -58,7 +58,7 @@
.cp { color: #969896 } /* Comment.Preproc */
.c1 { color: #969896 } /* Comment.Single */
.cs { color: #969896 } /* Comment.Special */
- .gd { color: #cc6666 } /* Generic.Deleted */
+ .gd { color: #c66 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
.gi { color: #b5bd68 } /* Generic.Inserted */
@@ -77,17 +77,17 @@
.na { color: #81a2be } /* Name.Attribute */
.nb { color: #c5c8c6 } /* Name.Builtin */
.nc { color: #f0c674 } /* Name.Class */
- .no { color: #cc6666 } /* Name.Constant */
+ .no { color: #c66 } /* Name.Constant */
.nd { color: #8abeb7 } /* Name.Decorator */
.ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #cc6666 } /* Name.Exception */
+ .ne { color: #c66 } /* Name.Exception */
.nf { color: #81a2be } /* Name.Function */
.nl { color: #c5c8c6 } /* Name.Label */
.nn { color: #f0c674 } /* Name.Namespace */
.nx { color: #81a2be } /* Name.Other */
.py { color: #c5c8c6 } /* Name.Property */
.nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #cc6666 } /* Name.Variable */
+ .nv { color: #c66 } /* Name.Variable */
.ow { color: #8abeb7 } /* Operator.Word */
.w { color: #c5c8c6 } /* Text.Whitespace */
.mf { color: #de935f } /* Literal.Number.Float */
@@ -106,8 +106,8 @@
.s1 { color: #b5bd68 } /* Literal.String.Single */
.ss { color: #b5bd68 } /* Literal.String.Symbol */
.bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #cc6666 } /* Name.Variable.Class */
- .vg { color: #cc6666 } /* Name.Variable.Global */
- .vi { color: #cc6666 } /* Name.Variable.Instance */
+ .vc { color: #c66 } /* Name.Variable.Class */
+ .vg { color: #c66 } /* Name.Variable.Global */
+ .vi { color: #c66 } /* Name.Variable.Instance */
.il { color: #de935f } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 9098e07adcd..806401c21ae 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -43,7 +43,7 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #49483e }
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 8b1a2824f76..6a809d4dfd2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -96,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 7ad89dd2c7c..b90c95c62d1 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -96,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #586e75 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 8a091028a6c..8c1b0cd84ec 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -23,7 +23,7 @@
.line_holder {
.diff-line-num {
&.old {
- background: #ffdddd;
+ background: #fdd;
border-color: #f1c0c0;
}
@@ -68,66 +68,66 @@
}
.hll { background-color: #f8f8f8 }
- .c { color: #999988; font-style: italic; }
+ .c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #999988; font-style: italic; }
- .cp { color: #999999; font-weight: bold; }
- .c1 { color: #999988; font-style: italic; }
- .cs { color: #999999; font-weight: bold; font-style: italic; }
- .gd { color: #000000; background-color: #ffdddd; }
- .gd .x { color: #000000; background-color: #ffaaaa; }
+ .cm { color: #998; font-style: italic; }
+ .cp { color: #999; font-weight: bold; }
+ .c1 { color: #998; font-style: italic; }
+ .cs { color: #999; font-weight: bold; font-style: italic; }
+ .gd { color: #000; background-color: #fdd; }
+ .gd .x { color: #000; background-color: #faa; }
.ge { font-style: italic; }
- .gr { color: #aa0000; }
- .gh { color: #999999; }
- .gi { color: #000000; background-color: #ddffdd; }
- .gi .x { color: #000000; background-color: #aaffaa; }
- .go { color: #888888; }
- .gp { color: #555555; }
+ .gr { color: #a00; }
+ .gh { color: #999; }
+ .gi { color: #000; background-color: #dfd; }
+ .gi .x { color: #000; background-color: #afa; }
+ .go { color: #888; }
+ .gp { color: #555; }
.gs { font-weight: bold; }
.gu { color: #800080; font-weight: bold; }
- .gt { color: #aa0000; }
+ .gt { color: #a00; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #445588; font-weight: bold; }
- .m { color: #009999; }
- .s { color: #dd1144; }
- .n { color: #333333; }
+ .kt { color: #458; font-weight: bold; }
+ .m { color: #099; }
+ .s { color: #d14; }
+ .n { color: #333; }
.na { color: teal; }
.nb { color: #0086b3; }
- .nc { color: #445588; font-weight: bold; }
+ .nc { color: #458; font-weight: bold; }
.no { color: teal; }
.ni { color: purple; }
- .ne { color: #990000; font-weight: bold; }
- .nf { color: #990000; font-weight: bold; }
- .nn { color: #555555; }
+ .ne { color: #900; font-weight: bold; }
+ .nf { color: #900; font-weight: bold; }
+ .nn { color: #555; }
.nt { color: navy; }
.nv { color: teal; }
.ow { font-weight: bold; }
- .w { color: #bbbbbb; }
- .mf { color: #009999; }
- .mh { color: #009999; }
- .mi { color: #009999; }
- .mo { color: #009999; }
- .sb { color: #dd1144; }
- .sc { color: #dd1144; }
- .sd { color: #dd1144; }
- .s2 { color: #dd1144; }
- .se { color: #dd1144; }
- .sh { color: #dd1144; }
- .si { color: #dd1144; }
- .sx { color: #dd1144; }
+ .w { color: #bbb; }
+ .mf { color: #099; }
+ .mh { color: #099; }
+ .mi { color: #099; }
+ .mo { color: #099; }
+ .sb { color: #d14; }
+ .sc { color: #d14; }
+ .sd { color: #d14; }
+ .s2 { color: #d14; }
+ .se { color: #d14; }
+ .sh { color: #d14; }
+ .si { color: #d14; }
+ .sx { color: #d14; }
.sr { color: #009926; }
- .s1 { color: #dd1144; }
+ .s1 { color: #d14; }
.ss { color: #990073; }
- .bp { color: #999999; }
+ .bp { color: #999; }
.vc { color: teal; }
.vg { color: teal; }
.vi { color: teal; }
- .il { color: #009999; }
- .gc { color: #999; background-color: #EAF2F5; }
+ .il { color: #099; }
+ .gc { color: #999; background-color: #eaf2f5; }
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 75f298019e3..201f3e5ca46 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,6 +1,6 @@
.build-page {
pre.trace {
- background: #111111;
+ background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index c0cc30d33a6..971656feb42 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -55,7 +55,7 @@
padding: 10px 0;
li {
- padding: 3px 0px;
+ padding: 3px 0;
line-height: 20px;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 818fd03e2ae..d57be1b2daa 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -9,7 +9,7 @@
.lists-separator {
margin: 10px 0;
- border-color: #DDD;
+ border-color: #ddd;
}
.commits-row {
@@ -76,7 +76,7 @@ li.commit {
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #EEE;
+ border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
background: #f9f9f9;
@@ -152,7 +152,7 @@ li.commit {
.count {
padding-top: 6px;
- padding-bottom: 0px;
+ padding-bottom: 0;
font-size: 12px;
color: #333;
display: block;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index a7925e79549..db06b8288c2 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -29,7 +29,7 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #FFF;
+ background: #fff;
color: #333;
.unfold {
@@ -57,8 +57,8 @@
font-family: $monospace_font;
border: none;
border-collapse: separate;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
@@ -76,10 +76,10 @@
}
.old_line, .new_line {
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
border: none;
- padding: 0px 5px;
+ padding: 0 5px;
border-right: 1px solid;
text-align: right;
min-width: 35px;
@@ -97,8 +97,8 @@
}
.line_content {
display: block;
- margin: 0px;
- padding: 0px 0.5em;
+ margin: 0;
+ padding: 0 0.5em;
border: none;
&.parallel {
display: table-cell;
@@ -118,7 +118,7 @@
background-color: #fff;
line-height: 0;
img {
- border: 1px solid #FFF;
+ border: 1px solid #fff;
background: image-url('trans_bg.gif');
max-width: 100%;
}
@@ -183,7 +183,7 @@
height: 14px;
width: 15px;
position: absolute;
- top: 0px;
+ top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
.bottom-handle {
@@ -191,7 +191,7 @@
height: 14px;
width: 15px;
position: absolute;
- bottom: 0px;
+ bottom: 0;
background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
@@ -206,8 +206,8 @@
.frame.added, .frame.deleted {
position: absolute;
display: block;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
}
.controls {
display: block;
@@ -215,7 +215,7 @@
width: 300px;
z-index: 100;
position: absolute;
- bottom: 0px;
+ bottom: 0;
left: 50%;
margin-left: -150px;
@@ -231,11 +231,11 @@
.dragger {
display: block;
position: absolute;
- left: 0px;
- top: 0px;
+ left: 0;
+ top: 0;
height: 14px;
width: 14px;
- background: image-url('onion_skin_sprites.gif') 0px -34px repeat-x;
+ background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x;
cursor: pointer;
}
@@ -243,17 +243,17 @@
display: block;
position: absolute;
top: 2px;
- right: 0px;
+ right: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px 0px no-repeat;
+ background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat;
}
.opaque {
display: block;
position: absolute;
top: 2px;
- left: 0px;
+ left: 0;
height: 10px;
width: 10px;
background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
@@ -265,7 +265,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #EEE;
+ background: #eee;
ul, li {
list-style: none;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 39d916cd336..43be5e38ba8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -14,9 +14,9 @@
}
.cancel-btn {
- color: #B94A48;
+ color: #b94a48;
&:hover {
- color: #B94A48;
+ color: #b94a48;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
index 6c721b514f8..b731abc7450 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -1,60 +1,60 @@
-.emoji-0023-20E3 { background-position: 0px 0px; }
-.emoji-002A-20E3 { background-position: -20px 0px; }
-.emoji-0030-20E3 { background-position: 0px -20px; }
+.emoji-0023-20E3 { background-position: 0 0; }
+.emoji-002A-20E3 { background-position: -20px 0; }
+.emoji-0030-20E3 { background-position: 0 -20px; }
.emoji-0031-20E3 { background-position: -20px -20px; }
-.emoji-0032-20E3 { background-position: -40px 0px; }
+.emoji-0032-20E3 { background-position: -40px 0; }
.emoji-0033-20E3 { background-position: -40px -20px; }
-.emoji-0034-20E3 { background-position: 0px -40px; }
+.emoji-0034-20E3 { background-position: 0 -40px; }
.emoji-0035-20E3 { background-position: -20px -40px; }
.emoji-0036-20E3 { background-position: -40px -40px; }
-.emoji-0037-20E3 { background-position: -60px 0px; }
+.emoji-0037-20E3 { background-position: -60px 0; }
.emoji-0038-20E3 { background-position: -60px -20px; }
.emoji-0039-20E3 { background-position: -60px -40px; }
-.emoji-00A9 { background-position: 0px -60px; }
+.emoji-00A9 { background-position: 0 -60px; }
.emoji-00AE { background-position: -20px -60px; }
.emoji-1F004 { background-position: -40px -60px; }
.emoji-1F0CF { background-position: -60px -60px; }
-.emoji-1F170 { background-position: -80px 0px; }
+.emoji-1F170 { background-position: -80px 0; }
.emoji-1F171 { background-position: -80px -20px; }
.emoji-1F17E { background-position: -80px -40px; }
.emoji-1F17F { background-position: -80px -60px; }
-.emoji-1F18E { background-position: 0px -80px; }
+.emoji-1F18E { background-position: 0 -80px; }
.emoji-1F191 { background-position: -20px -80px; }
.emoji-1F192 { background-position: -40px -80px; }
.emoji-1F193 { background-position: -60px -80px; }
.emoji-1F194 { background-position: -80px -80px; }
-.emoji-1F195 { background-position: -100px 0px; }
+.emoji-1F195 { background-position: -100px 0; }
.emoji-1F196 { background-position: -100px -20px; }
.emoji-1F197 { background-position: -100px -40px; }
.emoji-1F198 { background-position: -100px -60px; }
.emoji-1F199 { background-position: -100px -80px; }
-.emoji-1F19A { background-position: 0px -100px; }
+.emoji-1F19A { background-position: 0 -100px; }
.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
-.emoji-1F1E6-1F1EE { background-position: -120px 0px; }
+.emoji-1F1E6-1F1EE { background-position: -120px 0; }
.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
-.emoji-1F1E6-1F1F8 { background-position: 0px -120px; }
+.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
-.emoji-1F1E7-1F1E7 { background-position: -140px 0px; }
+.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
-.emoji-1F1E7-1F1EF { background-position: 0px -140px; }
+.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
@@ -62,7 +62,7 @@
.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
-.emoji-1F1E7-1F1F9 { background-position: -160px 0px; }
+.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
@@ -70,7 +70,7 @@
.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
-.emoji-1F1E8-1F1EB { background-position: 0px -160px; }
+.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
@@ -79,7 +79,7 @@
.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
-.emoji-1F1E8-1F1F5 { background-position: -180px 0px; }
+.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
@@ -88,7 +88,7 @@
.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
-.emoji-1F1E9-1F1EC { background-position: 0px -180px; }
+.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
@@ -98,7 +98,7 @@
.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
-.emoji-1F1EA-1F1ED { background-position: -200px 0px; }
+.emoji-1F1EA-1F1ED { background-position: -200px 0; }
.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
@@ -108,7 +108,7 @@
.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
-.emoji-1F1EB-1F1F7 { background-position: 0px -200px; }
+.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
@@ -119,7 +119,7 @@
.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
-.emoji-1F1EC-1F1F3 { background-position: -220px 0px; }
+.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
@@ -130,7 +130,7 @@
.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
-.emoji-1F1ED-1F1F3 { background-position: 0px -220px; }
+.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
@@ -142,7 +142,7 @@
.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
-.emoji-1F1EE-1F1F7 { background-position: -240px 0px; }
+.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
@@ -154,7 +154,7 @@
.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
-.emoji-1F1F0-1F1F3 { background-position: 0px -240px; }
+.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
@@ -167,7 +167,7 @@
.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
-.emoji-1F1F1-1F1F9 { background-position: -260px 0px; }
+.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
@@ -180,7 +180,7 @@
.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
-.emoji-1F1F2-1F1F2 { background-position: 0px -260px; }
+.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
@@ -194,7 +194,7 @@
.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
-.emoji-1F1F3-1F1E6 { background-position: -280px 0px; }
+.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
@@ -208,7 +208,7 @@
.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
-.emoji-1F1F5-1F1EA { background-position: 0px -280px; }
+.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
@@ -223,7 +223,7 @@
.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
-.emoji-1F1F7-1F1F4 { background-position: -300px 0px; }
+.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
@@ -238,7 +238,7 @@
.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
-.emoji-1F1F8-1F1F2 { background-position: 0px -300px; }
+.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
@@ -254,7 +254,7 @@
.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
-.emoji-1F1F9-1F1EF { background-position: -320px 0px; }
+.emoji-1F1F9-1F1EF { background-position: -320px 0; }
.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
@@ -270,7 +270,7 @@
.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
-.emoji-1F1FA-1F1FF { background-position: 0px -320px; }
+.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
@@ -287,7 +287,7 @@
.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
.emoji-1F201 { background-position: -320px -320px; }
-.emoji-1F202 { background-position: -340px 0px; }
+.emoji-1F202 { background-position: -340px 0; }
.emoji-1F21A { background-position: -340px -20px; }
.emoji-1F22F { background-position: -340px -40px; }
.emoji-1F232 { background-position: -340px -60px; }
@@ -304,7 +304,7 @@
.emoji-1F300 { background-position: -340px -280px; }
.emoji-1F301 { background-position: -340px -300px; }
.emoji-1F302 { background-position: -340px -320px; }
-.emoji-1F303 { background-position: 0px -340px; }
+.emoji-1F303 { background-position: 0 -340px; }
.emoji-1F304 { background-position: -20px -340px; }
.emoji-1F305 { background-position: -40px -340px; }
.emoji-1F306 { background-position: -60px -340px; }
@@ -322,7 +322,7 @@
.emoji-1F312 { background-position: -300px -340px; }
.emoji-1F313 { background-position: -320px -340px; }
.emoji-1F314 { background-position: -340px -340px; }
-.emoji-1F315 { background-position: -360px 0px; }
+.emoji-1F315 { background-position: -360px 0; }
.emoji-1F316 { background-position: -360px -20px; }
.emoji-1F317 { background-position: -360px -40px; }
.emoji-1F318 { background-position: -360px -60px; }
@@ -340,7 +340,7 @@
.emoji-1F326 { background-position: -360px -300px; }
.emoji-1F327 { background-position: -360px -320px; }
.emoji-1F328 { background-position: -360px -340px; }
-.emoji-1F329 { background-position: 0px -360px; }
+.emoji-1F329 { background-position: 0 -360px; }
.emoji-1F32A { background-position: -20px -360px; }
.emoji-1F32B { background-position: -40px -360px; }
.emoji-1F32C { background-position: -60px -360px; }
@@ -359,7 +359,7 @@
.emoji-1F339 { background-position: -320px -360px; }
.emoji-1F33A { background-position: -340px -360px; }
.emoji-1F33B { background-position: -360px -360px; }
-.emoji-1F33C { background-position: -380px 0px; }
+.emoji-1F33C { background-position: -380px 0; }
.emoji-1F33D { background-position: -380px -20px; }
.emoji-1F33E { background-position: -380px -40px; }
.emoji-1F33F { background-position: -380px -60px; }
@@ -378,7 +378,7 @@
.emoji-1F34C { background-position: -380px -320px; }
.emoji-1F34D { background-position: -380px -340px; }
.emoji-1F34E { background-position: -380px -360px; }
-.emoji-1F34F { background-position: 0px -380px; }
+.emoji-1F34F { background-position: 0 -380px; }
.emoji-1F350 { background-position: -20px -380px; }
.emoji-1F351 { background-position: -40px -380px; }
.emoji-1F352 { background-position: -60px -380px; }
@@ -398,7 +398,7 @@
.emoji-1F360 { background-position: -340px -380px; }
.emoji-1F361 { background-position: -360px -380px; }
.emoji-1F362 { background-position: -380px -380px; }
-.emoji-1F363 { background-position: -400px 0px; }
+.emoji-1F363 { background-position: -400px 0; }
.emoji-1F364 { background-position: -400px -20px; }
.emoji-1F365 { background-position: -400px -40px; }
.emoji-1F366 { background-position: -400px -60px; }
@@ -418,7 +418,7 @@
.emoji-1F374 { background-position: -400px -340px; }
.emoji-1F375 { background-position: -400px -360px; }
.emoji-1F376 { background-position: -400px -380px; }
-.emoji-1F377 { background-position: 0px -400px; }
+.emoji-1F377 { background-position: 0 -400px; }
.emoji-1F378 { background-position: -20px -400px; }
.emoji-1F379 { background-position: -40px -400px; }
.emoji-1F37A { background-position: -60px -400px; }
@@ -439,7 +439,7 @@
.emoji-1F385-1F3FE { background-position: -360px -400px; }
.emoji-1F385-1F3FF { background-position: -380px -400px; }
.emoji-1F386 { background-position: -400px -400px; }
-.emoji-1F387 { background-position: -420px 0px; }
+.emoji-1F387 { background-position: -420px 0; }
.emoji-1F388 { background-position: -420px -20px; }
.emoji-1F389 { background-position: -420px -40px; }
.emoji-1F38A { background-position: -420px -60px; }
@@ -460,7 +460,7 @@
.emoji-1F399 { background-position: -420px -360px; }
.emoji-1F39A { background-position: -420px -380px; }
.emoji-1F39B { background-position: -420px -400px; }
-.emoji-1F39C { background-position: 0px -420px; }
+.emoji-1F39C { background-position: 0 -420px; }
.emoji-1F39D { background-position: -20px -420px; }
.emoji-1F39E { background-position: -40px -420px; }
.emoji-1F39F { background-position: -60px -420px; }
@@ -482,7 +482,7 @@
.emoji-1F3AF { background-position: -380px -420px; }
.emoji-1F3B0 { background-position: -400px -420px; }
.emoji-1F3B1 { background-position: -420px -420px; }
-.emoji-1F3B2 { background-position: -440px 0px; }
+.emoji-1F3B2 { background-position: -440px 0; }
.emoji-1F3B3 { background-position: -440px -20px; }
.emoji-1F3B4 { background-position: -440px -40px; }
.emoji-1F3B5 { background-position: -440px -60px; }
@@ -504,7 +504,7 @@
.emoji-1F3C3-1F3FC { background-position: -440px -380px; }
.emoji-1F3C3-1F3FD { background-position: -440px -400px; }
.emoji-1F3C3-1F3FE { background-position: -440px -420px; }
-.emoji-1F3C3-1F3FF { background-position: 0px -440px; }
+.emoji-1F3C3-1F3FF { background-position: 0 -440px; }
.emoji-1F3C4 { background-position: -20px -440px; }
.emoji-1F3C4-1F3FB { background-position: -40px -440px; }
.emoji-1F3C4-1F3FC { background-position: -60px -440px; }
@@ -527,7 +527,7 @@
.emoji-1F3CA-1F3FD { background-position: -400px -440px; }
.emoji-1F3CA-1F3FE { background-position: -420px -440px; }
.emoji-1F3CA-1F3FF { background-position: -440px -440px; }
-.emoji-1F3CB { background-position: -460px 0px; }
+.emoji-1F3CB { background-position: -460px 0; }
.emoji-1F3CB-1F3FB { background-position: -460px -20px; }
.emoji-1F3CB-1F3FC { background-position: -460px -40px; }
.emoji-1F3CB-1F3FD { background-position: -460px -60px; }
@@ -550,7 +550,7 @@
.emoji-1F3DA { background-position: -460px -400px; }
.emoji-1F3DB { background-position: -460px -420px; }
.emoji-1F3DC { background-position: -460px -440px; }
-.emoji-1F3DD { background-position: 0px -460px; }
+.emoji-1F3DD { background-position: 0 -460px; }
.emoji-1F3DE { background-position: -20px -460px; }
.emoji-1F3DF { background-position: -40px -460px; }
.emoji-1F3E0 { background-position: -60px -460px; }
@@ -574,7 +574,7 @@
.emoji-1F3F2 { background-position: -420px -460px; }
.emoji-1F3F3 { background-position: -440px -460px; }
.emoji-1F3F4 { background-position: -460px -460px; }
-.emoji-1F3F5 { background-position: -480px 0px; }
+.emoji-1F3F5 { background-position: -480px 0; }
.emoji-1F3F6 { background-position: -480px -20px; }
.emoji-1F3F7 { background-position: -480px -40px; }
.emoji-1F3F8 { background-position: -480px -60px; }
@@ -598,7 +598,7 @@
.emoji-1F40A { background-position: -480px -420px; }
.emoji-1F40B { background-position: -480px -440px; }
.emoji-1F40C { background-position: -480px -460px; }
-.emoji-1F40D { background-position: 0px -480px; }
+.emoji-1F40D { background-position: 0 -480px; }
.emoji-1F40E { background-position: -20px -480px; }
.emoji-1F40F { background-position: -40px -480px; }
.emoji-1F410 { background-position: -60px -480px; }
@@ -623,7 +623,7 @@
.emoji-1F423 { background-position: -440px -480px; }
.emoji-1F424 { background-position: -460px -480px; }
.emoji-1F425 { background-position: -480px -480px; }
-.emoji-1F426 { background-position: -500px 0px; }
+.emoji-1F426 { background-position: -500px 0; }
.emoji-1F427 { background-position: -500px -20px; }
.emoji-1F428 { background-position: -500px -40px; }
.emoji-1F429 { background-position: -500px -60px; }
@@ -648,7 +648,7 @@
.emoji-1F43C { background-position: -500px -440px; }
.emoji-1F43D { background-position: -500px -460px; }
.emoji-1F43E { background-position: -500px -480px; }
-.emoji-1F43F { background-position: 0px -500px; }
+.emoji-1F43F { background-position: 0 -500px; }
.emoji-1F440 { background-position: -20px -500px; }
.emoji-1F441 { background-position: -40px -500px; }
.emoji-1F441-1F5E8 { background-position: -60px -500px; }
@@ -674,7 +674,7 @@
.emoji-1F446-1F3FF { background-position: -460px -500px; }
.emoji-1F447 { background-position: -480px -500px; }
.emoji-1F447-1F3FB { background-position: -500px -500px; }
-.emoji-1F447-1F3FC { background-position: -520px 0px; }
+.emoji-1F447-1F3FC { background-position: -520px 0; }
.emoji-1F447-1F3FD { background-position: -520px -20px; }
.emoji-1F447-1F3FE { background-position: -520px -40px; }
.emoji-1F447-1F3FF { background-position: -520px -60px; }
@@ -700,7 +700,7 @@
.emoji-1F44B-1F3FB { background-position: -520px -460px; }
.emoji-1F44B-1F3FC { background-position: -520px -480px; }
.emoji-1F44B-1F3FD { background-position: -520px -500px; }
-.emoji-1F44B-1F3FE { background-position: 0px -520px; }
+.emoji-1F44B-1F3FE { background-position: 0 -520px; }
.emoji-1F44B-1F3FF { background-position: -20px -520px; }
.emoji-1F44C { background-position: -40px -520px; }
.emoji-1F44C-1F3FB { background-position: -60px -520px; }
@@ -727,7 +727,7 @@
.emoji-1F44F-1F3FE { background-position: -480px -520px; }
.emoji-1F44F-1F3FF { background-position: -500px -520px; }
.emoji-1F450 { background-position: -520px -520px; }
-.emoji-1F450-1F3FB { background-position: -540px 0px; }
+.emoji-1F450-1F3FB { background-position: -540px 0; }
.emoji-1F450-1F3FC { background-position: -540px -20px; }
.emoji-1F450-1F3FD { background-position: -540px -40px; }
.emoji-1F450-1F3FE { background-position: -540px -60px; }
@@ -754,7 +754,7 @@
.emoji-1F464 { background-position: -540px -480px; }
.emoji-1F465 { background-position: -540px -500px; }
.emoji-1F466 { background-position: -540px -520px; }
-.emoji-1F466-1F3FB { background-position: 0px -540px; }
+.emoji-1F466-1F3FB { background-position: 0 -540px; }
.emoji-1F466-1F3FC { background-position: -20px -540px; }
.emoji-1F466-1F3FD { background-position: -40px -540px; }
.emoji-1F466-1F3FE { background-position: -60px -540px; }
@@ -782,7 +782,7 @@
.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; }
.emoji-1F468-2764-1F468 { background-position: -520px -540px; }
.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; }
-.emoji-1F469 { background-position: -560px 0px; }
+.emoji-1F469 { background-position: -560px 0; }
.emoji-1F469-1F3FB { background-position: -560px -20px; }
.emoji-1F469-1F3FC { background-position: -560px -40px; }
.emoji-1F469-1F3FD { background-position: -560px -60px; }
@@ -810,7 +810,7 @@
.emoji-1F470-1F3FB { background-position: -560px -500px; }
.emoji-1F470-1F3FC { background-position: -560px -520px; }
.emoji-1F470-1F3FD { background-position: -560px -540px; }
-.emoji-1F470-1F3FE { background-position: 0px -560px; }
+.emoji-1F470-1F3FE { background-position: 0 -560px; }
.emoji-1F470-1F3FF { background-position: -20px -560px; }
.emoji-1F471 { background-position: -40px -560px; }
.emoji-1F471-1F3FB { background-position: -60px -560px; }
@@ -839,7 +839,7 @@
.emoji-1F475 { background-position: -520px -560px; }
.emoji-1F475-1F3FB { background-position: -540px -560px; }
.emoji-1F475-1F3FC { background-position: -560px -560px; }
-.emoji-1F475-1F3FD { background-position: -580px 0px; }
+.emoji-1F475-1F3FD { background-position: -580px 0; }
.emoji-1F475-1F3FE { background-position: -580px -20px; }
.emoji-1F475-1F3FF { background-position: -580px -40px; }
.emoji-1F476 { background-position: -580px -60px; }
@@ -868,7 +868,7 @@
.emoji-1F47C-1F3FC { background-position: -580px -520px; }
.emoji-1F47C-1F3FD { background-position: -580px -540px; }
.emoji-1F47C-1F3FE { background-position: -580px -560px; }
-.emoji-1F47C-1F3FF { background-position: 0px -580px; }
+.emoji-1F47C-1F3FF { background-position: 0 -580px; }
.emoji-1F47D { background-position: -20px -580px; }
.emoji-1F47E { background-position: -40px -580px; }
.emoji-1F47F { background-position: -60px -580px; }
@@ -898,7 +898,7 @@
.emoji-1F485-1F3FD { background-position: -540px -580px; }
.emoji-1F485-1F3FE { background-position: -560px -580px; }
.emoji-1F485-1F3FF { background-position: -580px -580px; }
-.emoji-1F486 { background-position: -600px 0px; }
+.emoji-1F486 { background-position: -600px 0; }
.emoji-1F486-1F3FB { background-position: -600px -20px; }
.emoji-1F486-1F3FC { background-position: -600px -40px; }
.emoji-1F486-1F3FD { background-position: -600px -60px; }
@@ -928,7 +928,7 @@
.emoji-1F497 { background-position: -600px -540px; }
.emoji-1F498 { background-position: -600px -560px; }
.emoji-1F499 { background-position: -600px -580px; }
-.emoji-1F49A { background-position: 0px -600px; }
+.emoji-1F49A { background-position: 0 -600px; }
.emoji-1F49B { background-position: -20px -600px; }
.emoji-1F49C { background-position: -40px -600px; }
.emoji-1F49D { background-position: -60px -600px; }
@@ -959,7 +959,7 @@
.emoji-1F4B1 { background-position: -560px -600px; }
.emoji-1F4B2 { background-position: -580px -600px; }
.emoji-1F4B3 { background-position: -600px -600px; }
-.emoji-1F4B4 { background-position: -620px 0px; }
+.emoji-1F4B4 { background-position: -620px 0; }
.emoji-1F4B5 { background-position: -620px -20px; }
.emoji-1F4B6 { background-position: -620px -40px; }
.emoji-1F4B7 { background-position: -620px -60px; }
@@ -990,7 +990,7 @@
.emoji-1F4D0 { background-position: -620px -560px; }
.emoji-1F4D1 { background-position: -620px -580px; }
.emoji-1F4D2 { background-position: -620px -600px; }
-.emoji-1F4D3 { background-position: 0px -620px; }
+.emoji-1F4D3 { background-position: 0 -620px; }
.emoji-1F4D4 { background-position: -20px -620px; }
.emoji-1F4D5 { background-position: -40px -620px; }
.emoji-1F4D6 { background-position: -60px -620px; }
@@ -1022,7 +1022,7 @@
.emoji-1F4F0 { background-position: -580px -620px; }
.emoji-1F4F1 { background-position: -600px -620px; }
.emoji-1F4F2 { background-position: -620px -620px; }
-.emoji-1F4F3 { background-position: -640px 0px; }
+.emoji-1F4F3 { background-position: -640px 0; }
.emoji-1F4F4 { background-position: -640px -20px; }
.emoji-1F4F5 { background-position: -640px -40px; }
.emoji-1F4F6 { background-position: -640px -60px; }
@@ -1054,7 +1054,7 @@
.emoji-1F510 { background-position: -640px -580px; }
.emoji-1F511 { background-position: -640px -600px; }
.emoji-1F512 { background-position: -640px -620px; }
-.emoji-1F513 { background-position: 0px -640px; }
+.emoji-1F513 { background-position: 0 -640px; }
.emoji-1F514 { background-position: -20px -640px; }
.emoji-1F515 { background-position: -40px -640px; }
.emoji-1F516 { background-position: -60px -640px; }
@@ -1087,7 +1087,7 @@
.emoji-1F531 { background-position: -600px -640px; }
.emoji-1F532 { background-position: -620px -640px; }
.emoji-1F533 { background-position: -640px -640px; }
-.emoji-1F534 { background-position: -660px 0px; }
+.emoji-1F534 { background-position: -660px 0; }
.emoji-1F535 { background-position: -660px -20px; }
.emoji-1F536 { background-position: -660px -40px; }
.emoji-1F537 { background-position: -660px -60px; }
@@ -1120,7 +1120,7 @@
.emoji-1F55B { background-position: -660px -600px; }
.emoji-1F55C { background-position: -660px -620px; }
.emoji-1F55D { background-position: -660px -640px; }
-.emoji-1F55E { background-position: 0px -660px; }
+.emoji-1F55E { background-position: 0 -660px; }
.emoji-1F55F { background-position: -20px -660px; }
.emoji-1F560 { background-position: -40px -660px; }
.emoji-1F561 { background-position: -60px -660px; }
@@ -1154,7 +1154,7 @@
.emoji-1F578 { background-position: -620px -660px; }
.emoji-1F579 { background-position: -640px -660px; }
.emoji-1F57B { background-position: -660px -660px; }
-.emoji-1F57E { background-position: -680px 0px; }
+.emoji-1F57E { background-position: -680px 0; }
.emoji-1F57F { background-position: -680px -20px; }
.emoji-1F581 { background-position: -680px -40px; }
.emoji-1F582 { background-position: -680px -60px; }
@@ -1188,7 +1188,7 @@
.emoji-1F595-1F3FF { background-position: -680px -620px; }
.emoji-1F596 { background-position: -680px -640px; }
.emoji-1F596-1F3FB { background-position: -680px -660px; }
-.emoji-1F596-1F3FC { background-position: 0px -680px; }
+.emoji-1F596-1F3FC { background-position: 0 -680px; }
.emoji-1F596-1F3FD { background-position: -20px -680px; }
.emoji-1F596-1F3FE { background-position: -40px -680px; }
.emoji-1F596-1F3FF { background-position: -60px -680px; }
@@ -1223,7 +1223,7 @@
.emoji-1F5C4 { background-position: -640px -680px; }
.emoji-1F5C6 { background-position: -660px -680px; }
.emoji-1F5C7 { background-position: -680px -680px; }
-.emoji-1F5C9 { background-position: -700px 0px; }
+.emoji-1F5C9 { background-position: -700px 0; }
.emoji-1F5CA { background-position: -700px -20px; }
.emoji-1F5CE { background-position: -700px -40px; }
.emoji-1F5CF { background-position: -700px -60px; }
@@ -1258,7 +1258,7 @@
.emoji-1F5F8 { background-position: -700px -640px; }
.emoji-1F5F9 { background-position: -700px -660px; }
.emoji-1F5FA { background-position: -700px -680px; }
-.emoji-1F5FB { background-position: 0px -700px; }
+.emoji-1F5FB { background-position: 0 -700px; }
.emoji-1F5FC { background-position: -20px -700px; }
.emoji-1F5FD { background-position: -40px -700px; }
.emoji-1F5FE { background-position: -60px -700px; }
@@ -1294,7 +1294,7 @@
.emoji-1F61C { background-position: -660px -700px; }
.emoji-1F61D { background-position: -680px -700px; }
.emoji-1F61E { background-position: -700px -700px; }
-.emoji-1F61F { background-position: -720px 0px; }
+.emoji-1F61F { background-position: -720px 0; }
.emoji-1F620 { background-position: -720px -20px; }
.emoji-1F621 { background-position: -720px -40px; }
.emoji-1F622 { background-position: -720px -60px; }
@@ -1330,7 +1330,7 @@
.emoji-1F640 { background-position: -720px -660px; }
.emoji-1F641 { background-position: -720px -680px; }
.emoji-1F642 { background-position: -720px -700px; }
-.emoji-1F643 { background-position: 0px -720px; }
+.emoji-1F643 { background-position: 0 -720px; }
.emoji-1F644 { background-position: -20px -720px; }
.emoji-1F645 { background-position: -40px -720px; }
.emoji-1F645-1F3FB { background-position: -60px -720px; }
@@ -1367,7 +1367,7 @@
.emoji-1F64C-1F3FF { background-position: -680px -720px; }
.emoji-1F64D { background-position: -700px -720px; }
.emoji-1F64D-1F3FB { background-position: -720px -720px; }
-.emoji-1F64D-1F3FC { background-position: -740px 0px; }
+.emoji-1F64D-1F3FC { background-position: -740px 0; }
.emoji-1F64D-1F3FD { background-position: -740px -20px; }
.emoji-1F64D-1F3FE { background-position: -740px -40px; }
.emoji-1F64D-1F3FF { background-position: -740px -60px; }
@@ -1404,7 +1404,7 @@
.emoji-1F692 { background-position: -740px -680px; }
.emoji-1F693 { background-position: -740px -700px; }
.emoji-1F694 { background-position: -740px -720px; }
-.emoji-1F695 { background-position: 0px -740px; }
+.emoji-1F695 { background-position: 0 -740px; }
.emoji-1F696 { background-position: -20px -740px; }
.emoji-1F697 { background-position: -40px -740px; }
.emoji-1F698 { background-position: -60px -740px; }
@@ -1442,7 +1442,7 @@
.emoji-1F6B3 { background-position: -700px -740px; }
.emoji-1F6B4 { background-position: -720px -740px; }
.emoji-1F6B4-1F3FB { background-position: -740px -740px; }
-.emoji-1F6B4-1F3FC { background-position: -760px 0px; }
+.emoji-1F6B4-1F3FC { background-position: -760px 0; }
.emoji-1F6B4-1F3FD { background-position: -760px -20px; }
.emoji-1F6B4-1F3FE { background-position: -760px -40px; }
.emoji-1F6B4-1F3FF { background-position: -760px -60px; }
@@ -1480,7 +1480,7 @@
.emoji-1F6C5 { background-position: -760px -700px; }
.emoji-1F6C6 { background-position: -760px -720px; }
.emoji-1F6C7 { background-position: -760px -740px; }
-.emoji-1F6C8 { background-position: 0px -760px; }
+.emoji-1F6C8 { background-position: 0 -760px; }
.emoji-1F6C9 { background-position: -20px -760px; }
.emoji-1F6CA { background-position: -40px -760px; }
.emoji-1F6CB { background-position: -60px -760px; }
@@ -1519,7 +1519,7 @@
.emoji-1F918-1F3FC { background-position: -720px -760px; }
.emoji-1F918-1F3FD { background-position: -740px -760px; }
.emoji-1F918-1F3FE { background-position: -760px -760px; }
-.emoji-1F918-1F3FF { background-position: -780px 0px; }
+.emoji-1F918-1F3FF { background-position: -780px 0; }
.emoji-1F980 { background-position: -780px -20px; }
.emoji-1F981 { background-position: -780px -40px; }
.emoji-1F982 { background-position: -780px -60px; }
@@ -1558,7 +1558,7 @@
.emoji-24C2 { background-position: -780px -720px; }
.emoji-25AA { background-position: -780px -740px; }
.emoji-25AB { background-position: -780px -760px; }
-.emoji-25B6 { background-position: 0px -780px; }
+.emoji-25B6 { background-position: 0 -780px; }
.emoji-25C0 { background-position: -20px -780px; }
.emoji-25FB { background-position: -40px -780px; }
.emoji-25FC { background-position: -60px -780px; }
@@ -1598,7 +1598,7 @@
.emoji-264D { background-position: -740px -780px; }
.emoji-264E { background-position: -760px -780px; }
.emoji-264F { background-position: -780px -780px; }
-.emoji-2650 { background-position: -800px 0px; }
+.emoji-2650 { background-position: -800px 0; }
.emoji-2651 { background-position: -800px -20px; }
.emoji-2652 { background-position: -800px -40px; }
.emoji-2653 { background-position: -800px -60px; }
@@ -1638,7 +1638,7 @@
.emoji-26F0 { background-position: -800px -740px; }
.emoji-26F1 { background-position: -800px -760px; }
.emoji-26F2 { background-position: -800px -780px; }
-.emoji-26F3 { background-position: 0px -800px; }
+.emoji-26F3 { background-position: 0 -800px; }
.emoji-26F4 { background-position: -20px -800px; }
.emoji-26F5 { background-position: -40px -800px; }
.emoji-26F7 { background-position: -60px -800px; }
@@ -1679,7 +1679,7 @@
.emoji-270D-1F3FD { background-position: -760px -800px; }
.emoji-270D-1F3FE { background-position: -780px -800px; }
.emoji-270D-1F3FF { background-position: -800px -800px; }
-.emoji-270F { background-position: -820px 0px; }
+.emoji-270F { background-position: -820px 0; }
.emoji-2712 { background-position: -820px -20px; }
.emoji-2714 { background-position: -820px -40px; }
.emoji-2716 { background-position: -820px -60px; }
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 35df9a61c86..e7da0a2f689 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -63,7 +63,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -83,10 +83,10 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #F9F9F9;
+ background: #f9f9f9;
margin-left: 10px;
top: -6px;
img {
@@ -160,7 +160,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index c3b10d144e1..4e5c4ed84b6 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -6,11 +6,11 @@
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #EEE;
+ background: #eee;
}
.network-graph {
- background: #FFF;
+ background: #fff;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 3df4bb84bd2..6a99cd9cb94 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,6 +1,6 @@
i.icon-gitorious {
display: inline-block;
- background-position: 0px 0px;
+ background-position: 0 0;
background-size: contain;
background-repeat: no-repeat;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 1310e6ad7c7..faa2ebfda78 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -222,7 +222,7 @@
width: 100%;
text-align: center;
padding-bottom: 10px;
- color: #999999;
+ color: #999;
span {
display: block;
@@ -237,7 +237,7 @@
}
i {
- color: #999999;
+ color: #999;
}
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 1b686c58eaf..73718ff511a 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -68,18 +68,18 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
- background: #EFE;
- border-color: #CEC;
+ background: #efe;
+ border-color: #cec;
}
&.closed {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
&.merged {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 5ec0966194c..61ee34b695e 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -41,3 +41,7 @@
.color-label {
padding: 3px 4px;
}
+
+.label-subscription {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 61bec02f6c5..d9c47881265 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -39,7 +39,7 @@
.login-box{
background: #fafafa;
border-radius: 10px;
- box-shadow: 0 0px 2px #CCC;
+ box-shadow: 0 0 2px #ccc;
padding: 15px;
.login-heading h3 {
@@ -74,7 +74,7 @@
&.top {
@include border-radius(5px 5px 0 0);
- margin-bottom: 0px;
+ margin-bottom: 0;
}
&.bottom {
@@ -85,12 +85,12 @@
&.middle {
border-top: 0;
- margin-bottom:0px;
+ margin-bottom:0;
@include border-radius(0);
}
&:active, &:focus {
- background-color: #FFF;
+ background-color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2772623f4bd..cee5c47cfb2 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -113,7 +113,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #EEE;
+ border-top: 1px solid #eee;
}
.ci-coverage {
@@ -222,7 +222,7 @@
margin-bottom: 20px;
span {
- color: #B2B2B2;
+ color: #b2b2b2;
a {
color: $md-link-color;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 158c2a47862..61783ec46aa 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -156,7 +156,7 @@
.comment-hints {
color: #999;
- background: #FFF;
+ background: #fff;
padding: 7px;
margin-top: -7px;
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d5f9852ebed..969c79a9be9 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -11,8 +11,8 @@
ul.notes {
display: block;
list-style: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
.timeline-icon {
float: left;
@@ -30,7 +30,7 @@ ul.notes {
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
- background: #FDFDFD;
+ background: #fdfdfd;
.timeline-icon {
.avatar {
@@ -129,7 +129,7 @@ ul.notes {
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#F5F5F5, 8%);
+ border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
}
@@ -166,7 +166,7 @@ ul.notes {
vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: #FFF;
+ background: #fff;
color: $text-color;
}
&.notes_line2 {
@@ -232,7 +232,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
- background: #FFF;
+ background: #fff;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -249,7 +249,7 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #FFF;
+ color: #fff;
@include show-add-diff-note;
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 248c56e459d..260179074cf 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -30,7 +30,7 @@
.btn {
line-height: 40px;
height: 42px;
- padding: 0px 12px;
+ padding: 0 12px;
img {
width: 32px;
@@ -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/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index b1b76edfb32..3fe2c9a3346 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -85,7 +85,7 @@
.project-repo-buttons {
margin-top: 20px;
- margin-bottom: 0px;
+ margin-bottom: 0;
.count-buttons {
display: block;
@@ -140,7 +140,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #FFF;
+ border-right-color: #fff;
}
}
.count {
@@ -162,10 +162,10 @@
cursor: pointer;
background-image: none;
white-space: nowrap;
- margin: 0 11px 0px 4px;
+ margin: 0 11px 0 4px;
&:hover {
- background: #FFF;
+ background: #fff;
}
}
}
@@ -210,7 +210,7 @@
}
.project_member_row form {
- margin: 0px;
+ margin: 0;
}
.transfer-project .select2-container {
@@ -462,7 +462,7 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: #FFF;
+ background: #fff;
font-size: 14px;
margin-left: -1px;
cursor: auto;
@@ -472,16 +472,16 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: #E62958;
+ color: #e62958;
margin-top: 2px;
}
.private-forks-notice .private-fork-icon {
i:nth-child(1) {
- color: #2AA056;
+ color: #2aa056;
}
i:nth-child(2) {
- color: #FFFFFF;
+ color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index a9111a7388f..eec22c5dc96 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,7 +1,7 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #FFF;
+ color: #fff;
&.runner-state-shared {
background: #32b186;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 84234b15c65..b6e45024644 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -16,7 +16,7 @@
margin-bottom: 20px;
input {
- border-color: #BBB;
+ border-color: #bbb;
font-weight: bold;
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index 92d84d9640f..bed6470dbd3 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -13,13 +13,13 @@ table .sherlock-code {
}
.sherlock-line-samples-table {
- margin-bottom: 0px !important;
+ margin-bottom: 0 !important;
thead tr th,
tbody tr td {
font-size: 13px !important;
text-align: right;
- padding: 0px 10px !important;
+ padding: 0 10px !important;
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4b6ef035673..6f777d11641 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,7 +1,7 @@
.ci-status {
padding: 2px 7px;
margin-right: 5px;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
white-space: nowrap;
@include border-radius(4px);
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 479c3c16d46..27970eba159 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -61,7 +61,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -89,7 +89,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 05fa9312efb..587bd6a1e8a 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -7,7 +7,7 @@
.example {
&:before {
content: "Example";
- color: #BBB;
+ color: #bbb;
}
padding: 15px;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9a50096c0d0..8886c1dff56 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -2,23 +2,23 @@
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
- $black: #000000;
+ $black: #000;
$red: #cd0000;
$green: #00cd00;
$yellow: #cdcd00;
- $blue: #0000ee; // according to wikipedia, this is the xterm standard
+ $blue: #00e; // according to wikipedia, this is the xterm standard
//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
- $l-red: #ff0000;
- $l-green: #00ff00;
- $l-yellow: #ffff00;
+ $l-red: #f00;
+ $l-green: #0f0;
+ $l-yellow: #ff0;
$l-blue: #5c5cff;
- $l-magenta: #ff00ff;
- $l-cyan: #00ffff;
- $l-white: #ffffff;
+ $l-magenta: #f0f;
+ $l-cyan: #0ff;
+ $l-white: #fff;
.term-bold {
font-weight: bold;
@@ -136,7 +136,7 @@
.xterm-fg-0 {
- color: #000000;
+ color: #000;
}
.xterm-fg-1 {
color: #800000;
@@ -163,28 +163,28 @@
color: #808080;
}
.xterm-fg-9 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-10 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-11 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-12 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-13 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-14 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-15 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-16 {
- color: #000000;
+ color: #000;
}
.xterm-fg-17 {
color: #00005f;
@@ -199,7 +199,7 @@
color: #0000d7;
}
.xterm-fg-21 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-22 {
color: #005f00;
@@ -274,7 +274,7 @@
color: #00d7ff;
}
.xterm-fg-46 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-47 {
color: #00ff5f;
@@ -289,7 +289,7 @@
color: #00ffd7;
}
.xterm-fg-51 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-52 {
color: #5f0000;
@@ -724,7 +724,7 @@
color: #d7ffff;
}
.xterm-fg-196 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-197 {
color: #ff005f;
@@ -739,7 +739,7 @@
color: #ff00d7;
}
.xterm-fg-201 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-202 {
color: #ff5f00;
@@ -814,7 +814,7 @@
color: #ffd7ff;
}
.xterm-fg-226 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-227 {
color: #ffff5f;
@@ -829,7 +829,7 @@
color: #ffffd7;
}
.xterm-fg-231 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-232 {
color: #080808;
@@ -850,7 +850,7 @@
color: #3a3a3a;
}
.xterm-fg-238 {
- color: #444444;
+ color: #444;
}
.xterm-fg-239 {
color: #4e4e4e;
@@ -901,6 +901,6 @@
color: #e4e4e4;
}
.xterm-fg-255 {
- color: #eeeeee;
+ color: #eee;
}
}
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 2463cfa87be..e9b0972bdd8 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -6,7 +6,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def destroy
abuse_report = AbuseReport.find(params[:id])
- abuse_report.remove_user if params[:remove_user]
+ abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy
render nothing: true
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 87f4fb455b8..3063d299b1a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -119,10 +119,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
- DeleteUserService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
respond_to do |format|
- format.html { redirect_to admin_users_path }
+ format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
format.json { head :ok }
end
end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
new file mode 100644
index 00000000000..8a43c0b93c4
--- /dev/null
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -0,0 +1,17 @@
+module ToggleSubscriptionAction
+ extend ActiveSupport::Concern
+
+ def toggle_subscription
+ return unless current_user
+
+ subscribable_resource.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
+ private
+
+ def subscribable_resource
+ raise NotImplementedError
+ end
+end
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/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 67faa1e4437..b0a03ee45cc 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,6 +1,8 @@
class Projects::IssuesController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
- before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
+ before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
before_action :authorize_read_issue!
@@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
- def toggle_subscription
- @issue.toggle_subscription(current_user)
-
- render nothing: true
- end
-
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
@@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old
end
end
+ alias_method :subscribable_resource, :issue
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index ecac3c395ec..40d8098690a 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,8 +1,12 @@
class Projects::LabelsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, except: [:index]
+ before_action :authorize_admin_labels!, only: [
+ :new, :create, :edit, :update, :generate, :destroy
+ ]
respond_to :js, :html
@@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController
end
def label
- @label = @project.labels.find(params[:id])
+ @label ||= @project.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 03ba289eb94..61b82c9db46 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,10 +1,11 @@
class Projects::MergeRequestsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
include DiffHelper
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
+ :ci_status, :cancel_merge_when_build_succeeds
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
@@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
- def toggle_subscription
- @merge_request.toggle_subscription(current_user)
-
- render nothing: true
- end
-
protected
def selected_target_project
@@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
+ alias_method :subscribable_resource, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index c70add86a20..36f37221c58 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -172,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
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/labels_helper.rb b/app/helpers/labels_helper.rb
index 89a054289e8..4455dcd0e20 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -124,6 +124,14 @@ module LabelsHelper
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
+ def label_subscription_status(label)
+ label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
+
+ def label_subscription_toggle_button_text(label)
+ label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :render_colored_cross_project_label,
:text_color_for_bg, :escape_once
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/issues.rb b/app/mailers/emails/issues.rb
index 4a88cb61132..5f9adb32e00 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -16,7 +16,15 @@ module Emails
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
- @updated_by = User.find updated_by_user_id
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
@@ -24,20 +32,12 @@ module Emails
setup_issue_mail(issue_id, recipient_id)
@issue_status = status
- @updated_by = User.find updated_by_user_id
+ @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
- def issue_thread_options(sender_id, recipient_id)
- {
- from: sender(sender_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})")
- }
- end
-
def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@@ -45,5 +45,13 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
+
+ def issue_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@issue.title} (##{@issue.iid})")
+ }
+ end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 325996e2e16..55bb4f65270 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -3,50 +3,43 @@ module Emails
def new_merge_request_email(recipient_id, merge_request_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- mail_new_thread(@merge_request,
- from: sender(@merge_request.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
private
@@ -54,11 +47,17 @@ module Emails
def setup_merge_request_mail(merge_request_id, recipient_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
+
+ def merge_request_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ }
+ end
end
end
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/abuse_report.rb b/app/models/abuse_report.rb
index cc59aa4e911..b61f5123127 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -19,9 +19,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
- def remove_user
+ def remove_user(deleted_by:)
user.block
- user.destroy
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
end
def notify
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/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 3c42f582937..86ab84615ba 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern
include Participable
include Mentionable
+ include Subscribable
include StripAttribute
included do
@@ -18,7 +19,6 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
- has_many :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -149,28 +149,10 @@ module Issuable
notes.awards.where(note: "thumbsup").count
end
- def subscribed?(user)
- subscription = subscriptions.find_by_user_id(user.id)
-
- if subscription
- return subscription.subscribed
- end
-
+ def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
- end
-
- def unsubscribe(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
- end
-
def to_hook_data(user)
hook_data = {
object_kind: self.class.name.underscore,
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
new file mode 100644
index 00000000000..d5a881b2445
--- /dev/null
+++ b/app/models/concerns/subscribable.rb
@@ -0,0 +1,44 @@
+# == Subscribable concern
+#
+# Users can subscribe to these models.
+#
+# Used by Issue, MergeRequest, Label
+#
+
+module Subscribable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :subscriptions, dependent: :destroy, as: :subscribable
+ end
+
+ def subscribed?(user)
+ if subscription = subscriptions.find_by_user_id(user.id)
+ subscription.subscribed
+ else
+ subscribed_without_subscriptions?(user)
+ end
+ end
+
+ # Override this method to define custom logic to consider a subscribable as
+ # subscribed without an explicit subscription record.
+ def subscribed_without_subscriptions?(user)
+ false
+ end
+
+ def subscribers
+ subscriptions.where(subscribed: true).map(&:user)
+ end
+
+ def toggle_subscription(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: !subscribed?(user))
+ end
+
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+end
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/label.rb b/app/models/label.rb
index 5ff644b8426..f7ffc0b7f36 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -14,6 +14,8 @@
class Label < ActiveRecord::Base
include Referable
+ include Subscribable
+
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
diff --git a/app/models/project.rb b/app/models/project.rb
index 79e0cc7b23d..89a55a510cd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -286,7 +286,14 @@ class Project < ActiveRecord::Base
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))
@@ -508,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
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c96e6f0b8ea..59b1b86d1fb 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -2,7 +2,7 @@ class ProjectWiki
include Gitlab::ShellAdapter
MARKUPS = {
- 'Markdown' => :md,
+ 'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
} unless defined?(MARKUPS)
@@ -47,7 +47,7 @@ class ProjectWiki
def wiki
@wiki ||= begin
Gollum::Wiki.new(path_to_repo)
- rescue Gollum::NoSuchPathError
+ rescue Rugged::OSError
create_repo!
end
end
@@ -90,7 +90,7 @@ class ProjectWiki
def create_page(title, content, format = :markdown, message = nil)
commit = commit_details(:created, message, title)
- wiki.write_page(title, format, content, commit)
+ wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
rescue Gollum::DuplicatePageError => e
@@ -101,7 +101,7 @@ class ProjectWiki
def update_page(page, content, format = :markdown, message = nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, page.name, format, content, commit)
+ wiki.update_page(page, page.name, format.to_sym, content, commit)
update_project_activity
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6441cd87e87..e555e97689d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -758,12 +758,15 @@ class Repository
def parse_search_result(result)
ref = nil
filename = nil
+ basename = nil
startline = 0
result.each_line.each_with_index do |line, index|
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
+ extname = File.extname(filename)
+ basename = filename.sub(/#{extname}$/, '')
break
end
end
@@ -776,6 +779,7 @@ class Repository
OpenStruct.new(
filename: filename,
+ basename: basename,
ref: ref,
startline: startline,
data: data
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index dd75d3ab8ba..dd800ce110f 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribable, polymorphic: true
- validates :user_id,
+ validates :user_id,
uniqueness: { scope: [:subscribable_id, :subscribable_type] },
presence: true
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8871b0ab9fa..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
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index dbd70dc5a44..526760779a4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -62,7 +62,7 @@ class WikiPage
# The raw content of this page.
def content
@attributes[:content] ||= if @page
- @page.raw_data
+ @page.text_data
end
end
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/delete_user_service.rb b/app/services/delete_user_service.rb
index 173e50c9206..ce79287e35a 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -5,18 +5,22 @@ class DeleteUserService
@current_user = current_user
end
- def execute(user)
- if user.solo_owned_groups.present?
+ def execute(user, options = {})
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- user
- else
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
- end
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ DestroyGroupService.new(group, current_user).execute
+ end
- user.destroy
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
+
+ user.destroy
end
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 9189de390a2..3c42ac61be4 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,12 @@ class DestroyGroupService
end
def execute
- @group.projects.each do |project|
+ group.projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
- @group.destroy
+ group.destroy
end
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/issuable_base_service.rb b/app/services/issuable_base_service.rb
index ca87dca4a70..18f76d3f650 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone)
end
- def create_labels_note(issuable, added_labels, removed_labels)
+ def create_labels_note(issuable, old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService
end
end
- def has_changes?(issuable, options = {})
+ def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
end
- old_labels = options[:old_labels]
- labels_changed = old_labels && issuable.labels != old_labels
+ labels_changed = issuable.labels != old_labels
attrs_changed || labels_changed
end
- def handle_common_system_notes(issuable, options = {})
+ def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
- old_labels = options[:old_labels]
- if old_labels && (issuable.labels != old_labels)
- create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
- end
+ create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 51ef9dfe610..3563cbaa997 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,8 +4,8 @@ module Issues
update(issue)
end
- def handle_changes(issue, options = {})
- if has_changes?(issue, options)
+ def handle_changes(issue, old_labels: [])
+ if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
@@ -23,6 +23,11 @@ module Issues
notification_service.reassigned_issue(issue, current_user)
todo_service.reassigned_issue(issue, current_user)
end
+
+ added_labels = issue.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_issue(issue, added_labels, current_user)
+ end
end
def reopen_service
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 6319ad805b6..477c64e7377 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -14,8 +14,8 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request, options = {})
- if has_changes?(merge_request, options)
+ def handle_changes(merge_request, old_labels: [])
+ if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -44,6 +44,15 @@ module MergeRequests
merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked
end
+
+ added_labels = merge_request.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_merge_request(
+ merge_request,
+ added_labels,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ca8a41d93b8..19a6779dea9 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -24,16 +24,17 @@ class NotificationService
end
end
- # When create an issue we should send next emails:
+ # When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * watchers of the issue's labels
#
def new_issue(issue, current_user)
new_resource_email(issue, issue.project, 'new_issue_email')
end
- # When we close an issue we should send next emails:
+ # When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
@@ -43,7 +44,7 @@ class NotificationService
close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
end
- # When we reassign an issue we should send next emails:
+ # When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
@@ -52,16 +53,25 @@ class NotificationService
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
end
+ # When we add labels to an issue we should send an email to:
+ #
+ # * watchers of the issue's labels
+ #
+ def relabeled_issue(issue, added_labels, current_user)
+ relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ end
- # When create a merge request we should send next emails:
+ # When create a merge request we should send an email to:
#
# * mr assignee if their notification level is not Disabled
+ # * project team members with notification level higher then Participating
+ # * watchers of the mr's labels
#
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
end
- # When we reassign a merge_request we should send next emails:
+ # When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
@@ -70,6 +80,14 @@ class NotificationService
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
end
+ # When we add labels to a merge request we should send an email to:
+ #
+ # * watchers of the mr's labels
+ #
+ def relabeled_merge_request(merge_request, added_labels, current_user)
+ relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
end
@@ -91,7 +109,8 @@ class NotificationService
reopen_resource_email(
merge_request,
merge_request.target_project,
- current_user, 'merge_request_status_email',
+ current_user,
+ 'merge_request_status_email',
'reopened'
)
end
@@ -348,19 +367,23 @@ class NotificationService
end
def add_subscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
+ return recipients unless target.respond_to? :subscribers
- subscriptions = target.subscriptions
+ recipients + target.subscribers
+ end
- if subscriptions.any?
- recipients + subscriptions.where(subscribed: true).map(&:user)
- else
- recipients
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers
end
+
+ recipients
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author)
+ recipients = build_recipients(target, project, target.author, action: :new)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -392,6 +415,15 @@ class NotificationService
end
end
+ def relabeled_resource_email(target, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ label_names = labels.map(&:name)
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, project, current_user, method, status)
recipients = build_recipients(target, project, current_user)
@@ -416,6 +448,11 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
+
+ if action == :new
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
@@ -423,6 +460,13 @@ class NotificationService
recipients.uniq
end
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients.delete(current_user)
+ recipients.uniq
+ end
+
def mailer
Notify
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/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/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/_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
index d707ad4272d..b1694c919d0 100644
--- a/app/views/groups/_shared_projects.html.haml
+++ b/app/views/groups/_shared_projects.html.haml
@@ -1,18 +1 @@
-- if projects.present?
- .panel.panel-default
- .panel-heading
- Projects shared with
- %strong #{@group.name}
- (#{projects.count})
- %ul.well-list
- - projects.each do |project|
- %li.project-row
- = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do
- %span.namespace-name
- - if project.namespace
- = project.namespace.human_name
- \/
- %span.project-name
- = truncate(project.name, length: 25)
- %span.arrow
- %i.icon-angle-right
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index de314a4190c..23a34ac36dd 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -27,24 +27,33 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
- - if @shared_projects.present?
- %li
- = link_to "#shared", 'data-toggle' => 'tab' do
- Shared Projects
-
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
+ .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
+
.tab-content
.tab-pane.active#projects
= render "projects", projects: @projects
- .tab-pane#shared
- = render "shared_projects", projects: @shared_projects
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
- else
%p.nav-links.no-top
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/notify.html.haml b/app/views/layouts/notify.html.haml
index 325c68c69dc..37b4d562966 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -42,12 +42,15 @@
- else
#{link_to "View it on GitLab", @target_url}.
%br
- -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
+ -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
If you'd like to receive fewer emails, you can
- - if @sent_notification && @sent_notification.unsubscribable?
- = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
- from this thread or
- adjust your notification settings.
+ - if @labels_url
+ adjust your #{link_to 'label subscriptions', @labels_url}.
+ - else
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
= email_action @target_url
diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb
index 855d37429d9..daf20a226dd 100644
--- a/app/views/notify/_reassigned_issuable_email.text.erb
+++ b/app/views/notify/_reassigned_issuable_email.text.erb
@@ -1,6 +1,6 @@
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %>
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml
new file mode 100644
index 00000000000..80a0de255be
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{'Label'.pluralize(@label_names.size)} added:
+ %em= @label_names.to_sentence
diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb
new file mode 100644
index 00000000000..6a83d79fd61
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.text.erb
@@ -0,0 +1,3 @@
+<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
+
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml
new file mode 100644
index 00000000000..b17b16e1814
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @issue
diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb
new file mode 100644
index 00000000000..eeced97f601
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @issue %>
diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml
new file mode 100644
index 00000000000..9eaa9afa5b1
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @merge_request
diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb
new file mode 100644
index 00000000000..87bc80ead32
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @merge_request %>
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/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index f7ddd30c5a9..4927d239c1e 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -10,6 +10,16 @@
= link_to_label(label) do
= pluralize label.open_issues_count, 'open issue'
+ - if current_user
+ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
+ .subscription-status{data: {status: label_subscription_status(label)}}
+ %button.btn.btn-sm.btn-info.subscribe-button
+ %span= label_subscription_toggle_button_text(label)
+
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+
+- if current_user
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index f5859481d46..235106c4f74 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -2,9 +2,9 @@
.blob-result
.file-holder
.file-title
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
- = wiki_blob.filename
+ = wiki_blob.basename
.file-content.code.term
= render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 4df96a06dbe..3eb0db276b2 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -16,7 +16,7 @@
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
= 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" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", 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
- if params[:milestone_title]
@@ -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/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9020a1330a3..23b1ed1e51b 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -98,7 +98,7 @@
%hr
- if current_user
- subscribed = issuable.subscribed?(current_user)
- .block.light
+ .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
.title.hide-collapsed
@@ -124,5 +124,5 @@
= clipboard_button(clipboard_text: project_ref)
:javascript
- new Subscription("#{toggle_subscription_path(issuable)}");
+ new Subscription('.subscription');
new IssuableContext();
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
new file mode 100644
index 00000000000..6ff361e4d80
--- /dev/null
+++ b/app/workers/delete_user_worker.rb
@@ -0,0 +1,10 @@
+class DeleteUserWorker
+ include Sidekiq::Worker
+
+ def perform(current_user_id, delete_user_id, options = {})
+ delete_user = User.find(delete_user_id)
+ current_user = User.find(current_user_id)
+
+ DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ end
+end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 05f127d622a..500b745f55e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -357,6 +357,12 @@ production: &base
# crowd_server_url: 'CROWD SERVER URL',
# application_name: 'YOUR_APP_NAME',
# application_password: 'YOUR_APP_PASSWORD' } }
+ #
+ # - { name: 'auth0',
+ # args: {
+ # client_id: 'YOUR_AUTH0_CLIENT_ID',
+ # client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ # namespace: 'YOUR_AUTH0_DOMAIN' } }
# SSO maximum session duration in seconds. Defaults to CAS default of 8 hours.
# cas3:
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
new file mode 100644
index 00000000000..703f24f93b2
--- /dev/null
+++ b/config/initializers/gollum.rb
@@ -0,0 +1,13 @@
+module Gollum
+ GIT_ADAPTER = "rugged"
+end
+require "gollum-lib"
+
+module Gollum
+ class Committer
+ # Patch for UTF-8 path
+ def method_missing(name, *args)
+ index.send(name, *args)
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 536397e318a..2ae282f48a6 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
@@ -675,6 +675,10 @@ Rails.application.routes.draw do
collection do
post :generate
end
+
+ member do
+ post :toggle_subscription
+ end
end
resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
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/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb
new file mode 100644
index 00000000000..67bf5b4f978
--- /dev/null
+++ b/db/migrate/20160316123110_ci_runners_token_index.rb
@@ -0,0 +1,13 @@
+class CiRunnersTokenIndex < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def change
+ args = [:ci_runners, :token]
+
+ if Gitlab::Database.postgresql?
+ args << { algorithm: :concurrently }
+ end
+
+ add_index(*args)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fa406f70907..2c27b228864 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160309140734) do
+ActiveRecord::Schema.define(version: 20160316123110) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -260,6 +260,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
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", using: :btree
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|
@@ -720,6 +721,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
@@ -774,9 +776,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.string "type"
t.string "title"
t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "active", null: false
t.text "properties"
t.boolean "template", default: false
t.boolean "push_events", default: true
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/ci/deployment/README.md b/doc/ci/deployment/README.md
index ffd841ca9e7..7d91ce6710f 100644
--- a/doc/ci/deployment/README.md
+++ b/doc/ci/deployment/README.md
@@ -89,7 +89,7 @@ We also use two secure variables:
In GitLab CI 7.12 a new feature was introduced: Secure Variables.
Secure Variables can added by going to `Project > Variables > Add Variable`.
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
-The variables that are defined in the project settings are send along with the build script to the runner.
+The variables that are defined in the project settings are sent along with the build script to the runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
It is also important that secret's value is hidden in the build log.
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/yaml/README.md b/doc/ci/yaml/README.md
index 4dd5fe1f764..a9b79bbdb1b 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
@@ -278,13 +279,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 |
@@ -437,15 +439,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`:
@@ -456,14 +461,14 @@ artifacts:
- .config
```
-Send all git untracked files:
+Send all Git untracked files:
```yaml
artifacts:
untracked: true
```
-Send all git untracked files and files in `binaries`:
+Send all Git untracked files and files in `binaries`:
```yaml
artifacts:
@@ -497,6 +502,275 @@ 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
+```
+
+## 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/development/scss_styleguide.md b/doc/development/scss_styleguide.md
new file mode 100644
index 00000000000..6c48c25448b
--- /dev/null
+++ b/doc/development/scss_styleguide.md
@@ -0,0 +1,194 @@
+# SCSS styleguide
+
+This style guide recommends best practices for SCSS to make styles easy to read,
+easy to maintain, and performant for the end-user.
+
+## Rules
+
+### Naming
+
+CSS classes should use the `lowercase-hyphenated` format rather than
+`snake_case` or `camelCase`.
+
+```scss
+// Bad
+.class_name {
+ color: #fff;
+}
+
+// Bad
+.className {
+ color: #fff;
+}
+
+// Good
+.class-name {
+ color: #fff;
+}
+```
+
+### Formatting
+
+You should always use a space before a brace, braces should be on the same
+line, each property should each get its own line, and there should be a space
+between the property and its value.
+
+```scss
+// Bad
+.container-item {
+ width: 100px; height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item
+{
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item{
+ width:100px;
+ height:100px;
+ margin-top:0;
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+```
+
+Note that there is an exception for single-line rulesets, although these are
+not typically recommended.
+
+```scss
+p { margin: 0; padding: 0; }
+```
+
+### Colors
+
+HEX (hexadecimal) colors short-form should use shortform where possible, and
+should use lower case letters to differenciate between letters and numbers, e.
+g. `#E3E3E3` vs. `#e3e3e3`.
+
+```scss
+// Bad
+p {
+ color: #ffffff;
+}
+
+// Bad
+p {
+ color: #FFFFFF;
+}
+
+// Good
+p {
+ color: #fff;
+}
+```
+
+### Indentation
+
+Indentation should always use two spaces for each indentation level.
+
+```scss
+// Bad, four spaces
+p {
+ color: #f00;
+}
+
+// Good
+p {
+ color: #f00;
+}
+```
+
+### Semicolons
+
+Always include semicolons after every property. When the stylesheets are
+minified, the semicolons will be removed automatically.
+
+```scss
+// Bad
+.container-item {
+ width: 100px;
+ height: 100px
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+}
+```
+
+### Shorthand
+
+The shorthand form should be used for properties that support it.
+
+```scss
+// Bad
+margin: 10px 15px 10px 15px;
+padding: 10px 10px 10px 10px;
+
+// Good
+margin: 10px 15px;
+padding: 10px;
+```
+
+### Zero Units
+
+Omit length units on zero values, they're unnecessary and not including them
+is slightly more performant.
+
+```scss
+// Bad
+.item-with-padding {
+ padding: 0px;
+}
+
+// Good
+.item-with-padding {
+ padding: 0;
+}
+```
+
+### Selectors with a `js-` Prefix
+Do not use any selector prefixed with `js-` for styling purposes. These
+selectors are intended for use only with JavaScript to allow for removal or
+renaming without breaking styling.
+
+## Linting
+
+We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the
+ruleset in `.scss-lint.yml`, which is located in the home directory of the
+project.
+
+To check if any warnings will be produced by your changes, you can run `rake
+scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
+catch any warnings.
+
+If the Rake task is throwing warnings you don't understand, SCSS Lint's
+documentation includes [a full list of their linters][scss-lint-documentation].
+
+### Fixing issues
+
+If you want to automate changing a large portion of the codebase to conform to
+the SCSS style guide, you can use [CSSComb][csscomb]. First install
+[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
+CSSComb globally (system-wide). Run it in the GitLab directory with
+`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
+
+Note that this won't fix every problem, but it should fix a majority.
+
+[csscomb]: https://github.com/csscomb/csscomb.js
+[node]: https://github.com/nodejs/node
+[npm]: https://www.npmjs.com/
+[scss-lint]: https://github.com/brigade/scss-lint
+[scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 4f011397269..aa989417c4b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -22,7 +22,7 @@ If the highest number stable branch is unclear please check the [GitLab Blog](ht
This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
-This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [doc/install/requirements.md](./requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
+This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation).
@@ -76,7 +76,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 1.7.10 or higher, for example 1.7.12 or 2.0.0
+ # Make sure Git is version 2.7.3 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -89,8 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz
- cd git-2.4.3/
+ curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.3.tar.gz
+ echo '30d067499b61caddedaf1a407b4947244f14d10842d100f7c7c6ea1c288280cd git-2.7.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.3.tar.gz
+ cd git-2.7.3/
./configure
make prefix=/usr/local all
@@ -143,7 +144,7 @@ use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
- echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -c - && \
+ echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
rm go1.5.3.linux-amd64.tar.gz
@@ -160,19 +161,12 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Install the database packages
sudo apt-get install -y postgresql postgresql-client libpq-dev
-
- # Login to PostgreSQL
- sudo -u postgres psql -d template1
-
+
# Create a user for GitLab
- # Do not type the 'template1=#', this is part of the prompt
- template1=# CREATE USER git CREATEDB;
+ sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;"
# Create the GitLab production database & grant all privileges on database
- template1=# CREATE DATABASE gitlabhq_production OWNER git;
-
- # Quit the database session
- template1=# \q
+ sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;"
# Try connecting to the new database with the new user
sudo -u git -H psql -d gitlabhq_production
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index d59b7f0e84d..03cb08dd1f1 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -22,7 +22,7 @@ For the installations options please see [the installation page on the GitLab we
- FreeBSD
On the above unsupported distributions is still possible to install GitLab yourself.
-Please see the [installation from source guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information.
+Please see the [installation from source guide](installation.md) and the [installation guides](https://about.gitlab.com/installation/) for more information.
### Non-Unix operating systems such as Windows
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
new file mode 100644
index 00000000000..e5247082a89
--- /dev/null
+++ b/doc/integration/auth0.md
@@ -0,0 +1,89 @@
+# Auth0 OmniAuth Provider
+
+To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an
+application.
+
+1. Sign in to the [Auth0 Console](https://manage.auth0.com). If you need to
+create an account, you can do so at the same link.
+
+1. Select "New App/API".
+
+1. Provide the Application Name ('GitLab' works fine).
+
+1. Once created, you should see the Quick Start options. Disregard them and
+select 'Settings' above the Quick Start options.
+
+1. At the top of the Settings screen, you should see your Domain, Client ID and
+Client Secret. Take note of these as you'll need to put them in the
+configuration file. For example:
+ - Domain: `test1234.auth0.com`
+ - Client ID: `t6X8L2465bNePWLOvt9yi41i`
+ - Client Secret: `KbveM3nqfjwCbrhaUy_gDu2dss8TIlHIdzlyf33pB7dEK5u_NyQdp65O_o02hXs2`
+
+1. Fill in the Allowed Callback URLs:
+ - http://`YOUR_GITLAB_URL`/users/auth/auth0/callback (or)
+ - https://`YOUR_GITLAB_URL`/users/auth/auth0/callback
+
+1. Fill in the Allowed Origins (CORS):
+ - http://`YOUR_GITLAB_URL` (or)
+ - https://`YOUR_GITLAB_URL`
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "auth0",
+ "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'',
+ client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ namespace: 'YOUR_AUTH0_DOMAIN'
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ - { name: 'auth0',
+ args: {
+ client_id: 'YOUR_AUTH0_CLIENT_ID',
+ client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ namespace: 'YOUR_AUTH0_DOMAIN'
+ }
+ }
+ ```
+
+1. Change `YOUR_AUTH0_CLIENT_ID` to the client ID from the Auth0 Console page
+from step 5.
+
+1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
+page from step 5.
+
+1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+for the changes to take effect.
+
+On the sign in page there should now be an Auth0 icon below the regular sign in
+form. Click the icon to begin the authentication process. Auth0 will ask the
+user to sign in and authorize the GitLab application. If everything goes well
+the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index ba47cb16265..25f35988305 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -28,6 +28,7 @@ contains some settings that are common for all providers.
- [SAML](saml.md)
- [Crowd](crowd.md)
- [Azure](azure.md)
+- [Auth0](auth0.md)
## Initial OmniAuth Configuration
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/labels.feature b/features/project/labels.feature
new file mode 100644
index 00000000000..955bc3d8b1b
--- /dev/null
+++ b/features/project/labels.feature
@@ -0,0 +1,15 @@
+@labels
+Feature: Labels
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" has labels: "bug", "feature", "enhancement"
+ When I visit project "Shop" labels page
+
+ @javascript
+ Scenario: I can subscribe to a label
+ Then I should see that I am not subscribed to the "bug" label
+ When I click button "Subscribe" for the "bug" label
+ Then I should see that I am subscribed to the "bug" label
+ When I click button "Unsubscribe" for the "bug" label
+ Then I should see that I am not subscribed to the "bug" label
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/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/labels.rb b/features/steps/project/labels.rb
new file mode 100644
index 00000000000..17944527e3a
--- /dev/null
+++ b/features/steps/project/labels.rb
@@ -0,0 +1,34 @@
+class Spinach::Features::Labels < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedProject
+ include SharedNote
+ include SharedPaths
+ include SharedMarkdown
+
+ step 'And I visit project "Shop" labels page' do
+ visit namespace_project_labels_path(project.namespace, project)
+ end
+
+ step 'I should see that I am subscribed to the "bug" label' do
+ expect(subscribe_button).to have_content 'Unsubscribe'
+ end
+
+ step 'I should see that I am not subscribed to the "bug" label' do
+ expect(subscribe_button).to have_content 'Subscribe'
+ end
+
+ step 'I click button "Unsubscribe" for the "bug" label' do
+ subscribe_button.click
+ end
+
+ step 'I click button "Subscribe" for the "bug" label' do
+ subscribe_button.click
+ end
+
+ private
+
+ def subscribe_button
+ first('.subscribe-button span')
+ 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/search.rb b/features/steps/search.rb
index 48ea3fa3876..0ad837ebe1d 100644
--- a/features/steps/search.rb
+++ b/features/steps/search.rb
@@ -100,7 +100,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
step 'I should see "test_wiki" link in the search results' do
page.within('.results') do
- find(:css, '.search-results').should have_link 'test_wiki.md'
+ expect(find(:css, '.search-results')).to have_link 'test_wiki'
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/support/capybara.rb b/features/support/capybara.rb
index f33379f76c9..fe9e39cf509 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -9,7 +9,7 @@ Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
-Capybara.default_wait_time = timeout
+Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = false
unless ENV['CI'] || ENV['CI_SERVER']
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 7204dca34ba..9805e53624e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -408,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/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/diff/parser.rb b/lib/gitlab/diff/parser.rb
index d0f6ba23ab4..d0815fc7eea 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -4,6 +4,8 @@ module Gitlab
include Enumerable
def parse(lines)
+ return [] if lines.blank?
+
@lines = lines
line_obj_index = 0
line_old = 1
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index ebe301c1fc7..cfaf4a129b1 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -47,6 +47,7 @@ namespace :gemojione do
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
+ system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 581ab26db79..27ed57efe55 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -913,7 +913,7 @@ namespace :gitlab do
end
def check_git_version
- required_version = Gitlab::VersionInfo.new(1, 7, 10)
+ required_version = Gitlab::VersionInfo.new(2, 7, 3)
current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version)))
puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
diff --git a/lib/tasks/scss-lint.rake b/lib/tasks/scss-lint.rake
new file mode 100644
index 00000000000..250fd8699e4
--- /dev/null
+++ b/lib/tasks/scss-lint.rake
@@ -0,0 +1,10 @@
+unless Rails.env.production?
+ require 'scss_lint/rake_task'
+
+ SCSSLint::RakeTask.new do |t|
+ t.config = '.scss-lint.yml'
+ # See https://github.com/brigade/scss-lint/issues/726
+ # Hack, otherwise linter won't respect scss_files option in config file.
+ t.files = []
+ end
+end
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/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/labels.rb b/spec/factories/labels.rb
index 6e70af10af3..ea2be8928d5 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -13,7 +13,7 @@
FactoryGirl.define do
factory :label do
- title "Bug"
+ sequence(:title) { |n| "label#{n}" }
color "#990000"
project
end
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/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/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/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index f576c39284e..cdff063a9ed 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -90,4 +90,9 @@ eos
end
end
end
+
+ context 'when lines is empty' do
+ it { expect(parser.parse([])).to eq([]) }
+ it { expect(parser.parse(nil)).to eq([]) }
+ 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/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 232a11245a6..f910424d85b 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -100,6 +100,34 @@ describe Notify do
end
end
+ describe 'that have been relabeled' do
+ subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
+
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+ end
+
+ it 'contains the names of the added labels' do
+ is_expected.to have_body_text /foo, bar, and baz/
+ end
+
+ it 'contains a link to the issue' do
+ is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ end
+ end
+
describe 'status changed' do
let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
@@ -219,6 +247,34 @@ describe Notify do
end
end
+ describe 'that have been relabeled' do
+ subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
+
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ end
+
+ it 'contains the names of the added labels' do
+ is_expected.to have_body_text /foo, bar, and baz/
+ end
+
+ it 'contains a link to the merge request' do
+ is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ end
+ end
+
describe 'status changed' do
let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 48c851ebbd6..6019af544d3 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -112,6 +112,10 @@ shared_examples 'an unsubscribeable thread' do
it { is_expected.to have_body_text /unsubscribe/ }
end
-shared_examples "a user cannot unsubscribe through footer link" do
+shared_examples 'a user cannot unsubscribe through footer link' do
it { is_expected.not_to have_body_text /unsubscribe/ }
end
+
+shared_examples 'an email with a labels subscriptions link in its footer' do
+ it { is_expected.to have_body_text /label subscriptions/ }
+end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 4799bbaa57c..ac12ab6c757 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -13,7 +13,8 @@
require 'rails_helper'
RSpec.describe AbuseReport, type: :model do
- subject { create(:abuse_report) }
+ subject { create(:abuse_report) }
+ let(:user) { create(:user) }
it { expect(subject).to be_valid }
@@ -31,17 +32,14 @@ RSpec.describe AbuseReport, type: :model do
describe '#remove_user' do
it 'blocks the user' do
- report = build(:abuse_report)
-
- allow(report.user).to receive(:destroy)
-
- expect { report.remove_user }.to change { report.user.blocked? }.to(true)
+ expect { subject.remove_user(deleted_by: user) }.to change { subject.user.blocked? }.to(true)
end
- it 'removes the user' do
- report = build(:abuse_report)
+ it 'lets a worker delete the user' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
+ delete_solo_owned_groups: true)
- expect { report.remove_user }.to change { User.count }.by(-1)
+ subject.remove_user(deleted_by: user)
end
end
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/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index aff384c2949..be29b6d66ff 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -113,6 +113,48 @@ describe Issue, "Issuable" do
end
end
+ describe '#subscribed?' do
+ context 'user is not a participant in the issue' do
+ before { allow(issue).to receive(:participants).with(user).and_return([]) }
+
+ it 'returns false when no subcription exists' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ issue.subscriptions.create(user: user, subscribed: true)
+
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ issue.subscriptions.create(user: user, subscribed: false)
+
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+ end
+
+ context 'user is a participant in the issue' do
+ before { allow(issue).to receive(:participants).with(user).and_return([user]) }
+
+ it 'returns false when no subcription exists' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ issue.subscriptions.create(user: user, subscribed: true)
+
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ issue.subscriptions.create(user: user, subscribed: false)
+
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+ end
+ end
+
describe "#to_hook_data" do
let(:data) { issue.to_hook_data(user) }
let(:project) { issue.project }
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
new file mode 100644
index 00000000000..e31fdb0bffb
--- /dev/null
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Subscribable, 'Subscribable' do
+ let(:resource) { create(:issue) }
+ let(:user) { create(:user) }
+
+ describe '#subscribed?' do
+ it 'returns false when no subcription exists' do
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user, subscribed: true)
+
+ expect(resource.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user, subscribed: false)
+
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+ end
+ describe '#subscribers' do
+ it 'returns [] when no subcribers exists' do
+ expect(resource.subscribers).to be_empty
+ end
+
+ it 'returns the subscribed users' do
+ resource.subscriptions.create(user: user, subscribed: true)
+ resource.subscriptions.create(user: create(:user), subscribed: false)
+
+ expect(resource.subscribers).to eq [user]
+ end
+ end
+
+ describe '#toggle_subscription' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user)).to be_falsey
+
+ resource.toggle_subscription(user)
+
+ expect(resource.subscribed?(user)).to be_truthy
+ end
+ end
+
+ describe '#unsubscribe' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user, subscribed: true)
+ expect(resource.subscribed?(user)).to be_truthy
+
+ resource.unsubscribe(user)
+
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 59c5ffa6b9c..b8b9a455b83 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -634,6 +634,12 @@ describe Project, models: true do
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
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 34866be3395..fc2ab2d9931 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -101,13 +101,29 @@ describe Repository, models: true do
end
describe 'parsing result' do
- subject { repository.parse_search_result(results.first) }
+ subject { repository.parse_search_result(search_result) }
+ let(:search_result) { results.first }
it { is_expected.to be_an OpenStruct }
it { expect(subject.filename).to eq('CHANGELOG') }
+ it { expect(subject.basename).to eq('CHANGELOG') }
it { expect(subject.ref).to eq('master') }
it { expect(subject.startline).to eq(186) }
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 909b6796591..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
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
new file mode 100644
index 00000000000..a65938fa03b
--- /dev/null
+++ b/spec/services/delete_user_service_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe DeleteUserService, services: true do
+ describe "Deletes a user and all their personal projects" do
+ let!(:user) { create(:user) }
+ let!(:current_user) { create(:user) }
+ let!(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace) }
+
+ context 'no options are given' do
+ it 'deletes the user' do
+ DeleteUserService.new(current_user).execute(user)
+
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'will delete the project in the near future' do
+ expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+
+ DeleteUserService.new(current_user).execute(user)
+ end
+ end
+
+ context "solo owned groups present" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ DeleteUserService.new(current_user).execute(user)
+ end
+
+ it 'does not delete the user' do
+ expect(User.find(user.id)).to eq user
+ end
+ end
+
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true)
+ end
+
+ it 'deletes solo owned groups' do
+ expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'deletes the user' do
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
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/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index e579e49dfa7..4ffe753fef5 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do
let(:user3) { create(:user) }
let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) }
let(:label) { create(:label) }
+ let(:label2) { create(:label) }
let(:project) { issue.project }
before do
@@ -48,7 +49,7 @@ describe Issues::UpdateService, services: true do
it { expect(@issue.assignee).to eq(user2) }
it { expect(@issue).to be_closed }
it { expect(@issue.labels.count).to eq(1) }
- it { expect(@issue.labels.first.title).to eq('Bug') }
+ it { expect(@issue.labels.first.title).to eq(label.name) }
it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries
@@ -148,6 +149,48 @@ describe Issues::UpdateService, services: true do
end
end
+ context 'when the issue is relabeled' do
+ let!(:non_subscriber) { create(:user) }
+ let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+
+ it 'sends notifications for subscribers of newly added labels' do
+ opts = { label_ids: [label.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ context 'when issue has the `label` label' do
+ before { issue.labels << label }
+
+ it 'does not send notifications for existing labels' do
+ opts = { label_ids: [label.id, label2.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ it 'does not send notifications for removed labels' do
+ opts = { label_ids: [label2.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+ end
+
context 'when Issue has tasks' do
before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 99703c7a8ec..cb8cff2fa8c 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -7,6 +7,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) }
let(:project) { merge_request.project }
let(:label) { create(:label) }
+ let(:label2) { create(:label) }
before do
project.team << [user, :master]
@@ -53,7 +54,7 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.assignee).to eq(user2) }
it { expect(@merge_request).to be_closed }
it { expect(@merge_request.labels.count).to eq(1) }
- it { expect(@merge_request.labels.first.title).to eq('Bug') }
+ it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') }
it 'should execute hooks with update action' do
@@ -176,6 +177,48 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'when the issue is relabeled' do
+ let!(:non_subscriber) { create(:user) }
+ let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+
+ it 'sends notifications for subscribers of newly added labels' do
+ opts = { label_ids: [label.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ context 'when issue has the `label` label' do
+ before { merge_request.labels << label }
+
+ it 'does not send notifications for existing labels' do
+ opts = { label_ids: [label.id, label2.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ it 'does not send notifications for removed labels' do
+ opts = { label_ids: [label2.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+ end
+
context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 2d0b5df4224..b5407397c1d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -224,6 +224,15 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
end
+
+ it "emails subscribers of the issue's labels" do
+ subscriber = create(:user)
+ label = create(:label, issues: [issue])
+ label.toggle_subscription(subscriber)
+ notification.new_issue(issue, @u_disabled)
+
+ should_email(subscriber)
+ end
end
describe :reassigned_issue do
@@ -296,6 +305,35 @@ describe NotificationService, services: true do
end
end
+ describe '#relabeled_issue' do
+ let(:label) { create(:label, issues: [issue]) }
+ let(:label2) { create(:label) }
+ let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+
+ it "emails subscribers of the issue's added labels only" do
+ notification.relabeled_issue(issue, [label2], @u_disabled)
+
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+
+ it "doesn't send email to anyone but subscribers of the given labels" do
+ notification.relabeled_issue(issue, [label2], @u_disabled)
+
+ should_not_email(issue.assignee)
+ should_not_email(issue.author)
+ should_not_email(@u_watcher)
+ should_not_email(@u_participant_mentioned)
+ should_not_email(@subscriber)
+ should_not_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+ end
+
describe :close_issue do
it 'should sent email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
@@ -349,6 +387,15 @@ describe NotificationService, services: true do
should_not_email(@u_participating)
should_not_email(@u_disabled)
end
+
+ it "emails subscribers of the merge request's labels" do
+ subscriber = create(:user)
+ label = create(:label, merge_requests: [merge_request])
+ label.toggle_subscription(subscriber)
+ notification.new_merge_request(merge_request, @u_disabled)
+
+ should_email(subscriber)
+ end
end
describe :reassigned_merge_request do
@@ -366,6 +413,35 @@ describe NotificationService, services: true do
end
end
+ describe :relabel_merge_request do
+ let(:label) { create(:label, merge_requests: [merge_request]) }
+ let(:label2) { create(:label) }
+ let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+
+ it "emails subscribers of the merge request's added labels only" do
+ notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+
+ it "doesn't send email to anyone but subscribers of the given labels" do
+ notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+
+ should_not_email(merge_request.assignee)
+ should_not_email(merge_request.author)
+ should_not_email(@u_watcher)
+ should_not_email(@u_participant_mentioned)
+ should_not_email(@subscriber)
+ should_not_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+ end
+
describe :closed_merge_request do
it do
notification.close_mr(merge_request, @u_disabled)
@@ -467,16 +543,4 @@ describe NotificationService, services: true do
# Make the watcher a subscriber to detect dupes
issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
end
-
- def sent_to_user?(user)
- ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
- end
-
- def should_email(user)
- expect(sent_to_user?(user)).to be_truthy
- end
-
- def should_not_email(user)
- expect(sent_to_user?(user)).to be_falsey
- end
end
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/spec/spec_helper.rb b/spec/spec_helper.rb
index 7d939ca7509..596d607f2a1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -32,6 +32,7 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request
config.include StubConfiguration
+ config.include EmailHelpers
config.include RelativeUrl, type: feature
config.include TestEnv
config.include ActiveJob::TestHelper
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 65d59e6813c..e1f90e17cce 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -10,7 +10,7 @@ Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
-Capybara.default_wait_time = timeout
+Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
unless ENV['CI'] || ENV['CI_SERVER']
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
new file mode 100644
index 00000000000..a85ab22ce36
--- /dev/null
+++ b/spec/support/email_helpers.rb
@@ -0,0 +1,13 @@
+module EmailHelpers
+ def sent_to_user?(user)
+ ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ end
+
+ def should_email(user)
+ expect(sent_to_user?(user)).to be_truthy
+ end
+
+ def should_not_email(user)
+ expect(sent_to_user?(user)).to be_falsey
+ end
+end
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index 692d219e9f1..b90fc112671 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -1,6 +1,6 @@
module WaitForAjax
def wait_for_ajax
- Timeout.timeout(Capybara.default_wait_time) do
+ Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_all_ajax_requests?
end
end
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
new file mode 100644
index 00000000000..14c56521280
--- /dev/null
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DeleteUserWorker do
+ let!(:user) { create(:user) }
+ let!(:current_user) { create(:user) }
+
+ it "calls the DeleteUserWorker with the params it was given" do
+ expect_any_instance_of(DeleteUserService).to receive(:execute).
+ with(user, {})
+
+ DeleteUserWorker.new.perform(current_user.id, user.id)
+ end
+
+ it "uses symbolized keys" do
+ expect_any_instance_of(DeleteUserService).to receive(:execute).
+ with(user, test: "test")
+
+ DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test")
+ 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;
-}