summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Lopez <james@jameslopez.es>2016-02-29 15:49:00 +0100
committerJames Lopez <james@jameslopez.es>2016-02-29 15:49:00 +0100
commit8fe72af3dc14a94e0823ed21016d005f509ea2a1 (patch)
tree8db096bd899aad335dfd5acfefa16f471586c1ab
parent303e9eb5bfa528e5df3f8b3e76596b2d4401acf4 (diff)
parent912e64381543c38ccf2e0d5f22b9986f49879278 (diff)
downloadgitlab-ce-8fe72af3dc14a94e0823ed21016d005f509ea2a1.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/contributions-forked-projects
-rw-r--r--.gitlab-ci.yml41
-rw-r--r--CHANGELOG30
-rw-r--r--CONTRIBUTING.md26
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/activities.js.coffee18
-rw-r--r--app/assets/javascripts/application.js.coffee5
-rw-r--r--app/assets/javascripts/autosave.js.coffee6
-rw-r--r--app/assets/javascripts/awards_handler.coffee16
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee5
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee12
-rw-r--r--app/assets/javascripts/milestone.js.coffee30
-rw-r--r--app/assets/javascripts/profile.js.coffee49
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee8
-rw-r--r--app/assets/javascripts/sidebar.js.coffee6
-rw-r--r--app/assets/javascripts/wikis.js.coffee3
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss17
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss7
-rw-r--r--app/assets/stylesheets/framework/header.scss9
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss9
-rw-r--r--app/assets/stylesheets/framework/jquery.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss24
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss13
-rw-r--r--app/assets/stylesheets/framework/selects.scss147
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss82
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss7
-rw-r--r--app/assets/stylesheets/pages/appearances.scss11
-rw-r--r--app/assets/stylesheets/pages/issuable.scss60
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss9
-rw-r--r--app/assets/stylesheets/pages/profile.scss55
-rw-r--r--app/assets/stylesheets/pages/projects.scss22
-rw-r--r--app/assets/stylesheets/pages/snippets.scss24
-rw-r--r--app/assets/stylesheets/pages/todos.scss35
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/controllers/admin/appearances_controller.rb57
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/refs_controller.rb6
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/helpers/appearances_helper.rb28
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/models/appearance.rb9
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/project.rb5
-rw-r--r--app/models/repository.rb49
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb8
-rw-r--r--app/services/git_push_service.rb6
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb12
-rw-r--r--app/services/notes/create_service.rb1
-rw-r--r--app/services/projects/destroy_service.rb6
-rw-r--r--app/services/todo_service.rb4
-rw-r--r--app/uploaders/avatar_uploader.rb11
-rw-r--r--app/validators/url_validator.rb3
-rw-r--r--app/views/admin/appearances/_form.html.haml58
-rw-r--r--app/views/admin/appearances/preview.html.haml29
-rw-r--r--app/views/admin/appearances/show.html.haml7
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/emojis/index.html.haml9
-rw-r--r--app/views/groups/_projects.html.haml11
-rw-r--r--app/views/groups/show.html.haml7
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/ui.html.haml216
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/nav/_admin.html.haml5
-rw-r--r--app/views/profiles/applications.html.haml2
-rw-r--r--app/views/profiles/audit_log.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml34
-rw-r--r--app/views/profiles/keys/index.html.haml10
-rw-r--r--app/views/profiles/notifications/show.html.haml3
-rw-r--r--app/views/profiles/passwords/edit.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml3
-rw-r--r--app/views/profiles/show.html.haml21
-rw-r--r--app/views/projects/blame/show.html.haml6
-rw-r--r--app/views/projects/builds/index.html.haml4
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/refs/logs_tree.js.haml7
-rw-r--r--app/views/projects/wikis/_main_links.html.haml23
-rw-r--r--app/views/projects/wikis/_nav.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml13
-rw-r--r--app/views/projects/wikis/edit.html.haml22
-rw-r--r--app/views/projects/wikis/history.html.haml13
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml11
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml6
-rw-r--r--app/views/shared/issuable/_participants.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml31
-rw-r--r--app/views/shared/projects/_list.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml11
-rw-r--r--app/views/shared/snippets/_snippet.html.haml7
-rw-r--r--app/views/snippets/_snippets.html.haml2
-rw-r--r--app/views/users/show.html.haml26
-rw-r--r--app/views/votes/_votes_block.html.haml9
-rw-r--r--app/workers/repository_fork_worker.rb2
-rw-r--r--app/workers/repository_import_worker.rb2
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/newrelic.yml16
-rw-r--r--config/routes.rb15
-rw-r--r--db/migrate/20160222153918_create_appearances_ce.rb14
-rw-r--r--db/schema.rb11
-rw-r--r--doc/README.md44
-rw-r--r--doc/ci/README.md72
-rw-r--r--doc/ci/examples/README.md16
-rw-r--r--doc/ci/examples/php.md (renamed from doc/ci/languages/php.md)0
-rw-r--r--doc/ci/languages/README.md7
-rw-r--r--doc/ci/quick_start/README.md8
-rw-r--r--doc/ci/services/README.md12
-rw-r--r--doc/ci/yaml/README.md7
-rw-r--r--doc/customization/branded_login_page.md19
-rw-r--r--doc/customization/branded_login_page/appearance.pngbin0 -> 365120 bytes
-rw-r--r--doc/customization/branded_login_page/custom_sign_in.pngbin0 -> 314111 bytes
-rw-r--r--doc/customization/branded_login_page/default_login_page.pngbin0 -> 292731 bytes
-rw-r--r--doc/customization/welcome_message.md8
-rw-r--r--doc/development/README.md11
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/gotchas.md103
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.png (renamed from doc/gitlab-basics/basicsimages/compare_braches.png)bin1624 -> 1624 bytes
-rw-r--r--doc/install/installation.md5
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/legal/individual_contributor_license_agreement.md2
-rw-r--r--doc/permissions/permissions.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/importing/migrating_from_svn.md1
-rw-r--r--doc/workflow/shortcuts.pngbin48782 -> 25005 bytes
-rw-r--r--features/admin/appearance.feature37
-rw-r--r--features/dashboard/event_filters.feature12
-rw-r--r--features/explore/projects.feature1
-rw-r--r--features/login_form.feature5
-rw-r--r--features/project/builds/summary.feature2
-rw-r--r--features/project/milestone.feature1
-rw-r--r--features/project/project.feature6
-rw-r--r--features/steps/admin/appearance.rb72
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/login_form.rb25
-rw-r--r--features/steps/profile/profile.rb22
-rw-r--r--features/steps/project/builds/summary.rb6
-rw-r--r--features/steps/project/issues/award_emoji.rb3
-rw-r--r--features/steps/project/project.rb10
-rw-r--r--features/steps/project/project_milestone.rb6
-rw-r--r--features/steps/project/source/markdown_render.rb18
-rw-r--r--features/steps/project/wiki.rb6
-rw-r--r--features/steps/shared/builds.rb6
-rw-r--r--features/steps/shared/paths.rb12
-rw-r--r--features/support/capybara.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb10
-rw-r--r--lib/tasks/gitlab/check.rake13
-rwxr-xr-xscripts/notify_slack.sh13
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb2
-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.rb7
-rw-r--r--spec/factories/appearances.rb8
-rw-r--r--spec/factories/ci/builds.rb6
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb4
-rw-r--r--spec/helpers/application_helper_spec.rb6
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb16
-rw-r--r--spec/lib/ci/status_spec.rb72
-rw-r--r--spec/models/appearance_spec.rb10
-rw-r--r--spec/models/commit_spec.rb34
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/web_hook_spec.rb20
-rw-r--r--spec/models/repository_spec.rb108
-rw-r--r--spec/models/todo_spec.rb (renamed from spec/models/todo_spec.rb.rb)34
-rw-r--r--spec/models/user_spec.rb12
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb16
-rw-r--r--spec/services/todo_service_spec.rb10
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb37
-rwxr-xr-xvendor/assets/javascripts/cropper.js2972
-rwxr-xr-xvendor/assets/stylesheets/cropper.css379
194 files changed, 5348 insertions, 865 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8a729f957a2..4e98b7a68ee 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,7 +24,12 @@ before_script:
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
+stages:
+- test
+- notifications
+
spec:feature:
+ stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
@@ -33,6 +38,7 @@ spec:feature:
- mysql
spec:api:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
@@ -40,6 +46,7 @@ spec:api:
- mysql
spec:models:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
tags:
@@ -47,6 +54,7 @@ spec:models:
- mysql
spec:lib:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
tags:
@@ -54,6 +62,7 @@ spec:lib:
- mysql
spec:services:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
tags:
@@ -61,6 +70,7 @@ spec:services:
- mysql
spec:benchmark:
+ stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
@@ -69,6 +79,7 @@ spec:benchmark:
allow_failure: true
spec:other:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
@@ -76,6 +87,7 @@ spec:other:
- mysql
spinach:project:half:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
tags:
@@ -83,6 +95,7 @@ spinach:project:half:
- mysql
spinach:project:rest:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
@@ -90,6 +103,7 @@ spinach:project:rest:
- mysql
spinach:other:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
@@ -97,6 +111,7 @@ spinach:other:
- mysql
teaspoon:
+ stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
tags:
@@ -104,6 +119,7 @@ teaspoon:
- mysql
rubocop:
+ stage: test
script:
- bundle exec rubocop
tags:
@@ -111,6 +127,7 @@ rubocop:
- mysql
brakeman:
+ stage: test
script:
- bundle exec rake brakeman
tags:
@@ -118,6 +135,7 @@ brakeman:
- mysql
flog:
+ stage: test
script:
- bundle exec rake flog
tags:
@@ -125,6 +143,7 @@ flog:
- mysql
flay:
+ stage: test
script:
- bundle exec rake flay
tags:
@@ -132,6 +151,7 @@ flay:
- mysql
bundler:audit:
+ stage: test
script:
- "bundle exec bundle-audit update"
- "bundle exec bundle-audit check"
@@ -143,6 +163,7 @@ bundler:audit:
# Ruby 2.2 jobs
spec:feature:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -158,6 +179,7 @@ spec:feature:ruby22:
- mysql
spec:api:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -172,6 +194,7 @@ spec:api:ruby22:
- mysql
spec:models:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -186,6 +209,7 @@ spec:models:ruby22:
- mysql
spec:lib:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -200,6 +224,7 @@ spec:lib:ruby22:
- mysql
spec:services:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -214,6 +239,7 @@ spec:services:ruby22:
- mysql
spec:benchmark:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -229,6 +255,7 @@ spec:benchmark:ruby22:
allow_failure: true
spec:other:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -243,6 +270,7 @@ spec:other:ruby22:
- mysql
spinach:project:half:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -257,6 +285,7 @@ spinach:project:half:ruby22:
- mysql
spinach:project:rest:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -271,6 +300,7 @@ spinach:project:rest:ruby22:
- mysql
spinach:other:ruby22:
+ stage: test
image: ruby:2.2
only:
- master
@@ -284,3 +314,14 @@ spinach:other:ruby22:
- ruby
- mysql
+
+notify:slack:
+ stage: notifications
+ script:
+ - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Check <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
+ when: on_failure
+ only:
+ - master@gitlab-org/gitlab-ce
+ - tags@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - tags@gitlab-org/gitlab-ee \ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
index 053bb962f76..56dc99b4e7d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,33 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
- Contributions to forked projects are included in calendar
+ - Improve the formatting for the user page bio (Connor Shea)
+ - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
+ - Strip leading and trailing spaces in URL validator (evuez)
+ - Update documentation to reflect Guest role not being enforced on internal projects
+
+v 8.5.2
+ - Fix sidebar overlapping content when screen width was below 1200px
+ - Fix error 500 when commenting on a commit
+
+v 8.5.1
+ - Fix group projects styles
+ - Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec)
+ - Fix a set of small UI glitches in project, profile, and wiki pages
+ - Restrict permissions on public/uploads
+ - Fix the merge request side-by-side view after loading diff results
+ - Fix the look of tooltip for the "Revert" button
+ - Add when the Builds & Runners API changes got introduced
+ - Fix error 500 on some merged merge requests
+ - Fix an issue causing the content of the issuable sidebar to disappear
+ - Fix error 500 when trying to mark an already done todo as "done"
+ - Fix an issue where MRs weren't sortable
+ - Issues can now be dragged & dropped into empty milestone lists. This is also
+ possible with MRs
+ - Changed padding & background color for highlighted notes
+ - Re-add the newrelic_rpm gem which was removed without any deprecation or warning (Stan Hu)
+ - Update sentry-raven gem to 0.15.6
+ - Add build coverage in project's builds page (Steffen Köhler)
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
@@ -82,6 +109,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.5
+ - No CE-specific changes
+
v 8.4.4
- Update omniauth-saml gem to 1.4.2
- Prevent long-running backup tasks from timing out the database connection
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c4522998f42..b13f36af214 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,7 +34,7 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
-If you have read this guide and want to know how the GitLab [core-team][]
+If you have read this guide and want to know how the GitLab [core team][core-team]
operates please see [the GitLab contributing process](PROCESS.md).
## Contributor license agreement
@@ -68,10 +68,10 @@ for audiences of all ages.
## Helping others
Please help other GitLab users when you can. The channels people will reach out
-on can be found on the [getting help page][].
+on can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
-respond in the IRC channel. You can also sign up on [CodeTriage][] to help with
+respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
the remaining issues on the GitHub issue tracker.
## I want to contribute!
@@ -115,7 +115,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members
-of the project cannot add labels. You can instead ask one of the [core team][]
+of the project cannot add labels. You can instead ask one of the [core team][core-team]
members to add the label `feature proposal` to the issue.
Please keep feature proposals as small and simple as possible, complex ones
@@ -299,8 +299,8 @@ to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it.
For examples of feedback on merge requests please look at already
-[closed merge requests][]. If you would like quick feedback on your merge
-request feel free to mention one of the Merge Marshalls of the [core team][].
+[closed merge requests][closed-merge-requests]. If you would like quick feedback on your merge
+request feel free to mention one of the Merge Marshalls of the [core team][core-team].
Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the
@@ -369,7 +369,7 @@ Like all merge requests the target should be master so all bugfixes are in maste
## Definition of done
If you contribute to GitLab please know that changes involve more than just
-code. We have the following [definition of done][]. Please ensure you support
+code. We have the following [definition of done][definition-of-done]. Please ensure you support
the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
@@ -448,12 +448,12 @@ when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
-This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0,
+This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
[core-team]: https://about.gitlab.com/core-team/
-[getting help page]: https://about.gitlab.com/getting-help/
-[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
+[getting-help]: https://about.gitlab.com/getting-help/
+[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
@@ -467,9 +467,9 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
-[closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
-[definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[Contributor Covenant]: http://contributor-covenant.org
+[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
+[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
+[contributor-covenant]: http://contributor-covenant.org
[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"
diff --git a/Gemfile b/Gemfile
index 1602ef871c7..e37651f6fb3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -77,6 +77,9 @@ gem "haml-rails", '~> 0.9.0'
# Files attachments
gem "carrierwave", '~> 0.9.0'
+# Image editing
+gem "mini_magick", '~> 4.4.0'
+
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -218,7 +221,7 @@ gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
# Sentry integration
-gem 'sentry-raven'
+gem 'sentry-raven', '~> 0.15'
# Metrics
group :metrics do
@@ -303,6 +306,8 @@ group :production do
gem "gitlab_meta", '7.0'
end
+gem "newrelic_rpm", '~> 3.14'
+
gem 'octokit', '~> 3.8.0'
gem "mail_room", "~> 0.6.1"
diff --git a/Gemfile.lock b/Gemfile.lock
index 4681adf2bd0..d0f780e9519 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -468,6 +468,7 @@ 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)
@@ -479,6 +480,7 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
+ newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
nprogress-rails (0.1.6.7)
@@ -722,7 +724,7 @@ GEM
activesupport (>= 3.1, < 4.3)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (0.15.4)
+ sentry-raven (0.15.6)
faraday (>= 0.7.6)
settingslogic (2.0.9)
sexp_processor (4.6.0)
@@ -954,11 +956,13 @@ 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)
nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
+ newrelic_rpm (~> 3.14)
nokogiri (~> 1.6.7, >= 1.6.7.2)
nprogress-rails (~> 0.1.6.7)
oauth2 (~> 1.0.0)
@@ -1007,7 +1011,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven
+ sentry-raven (~> 0.15)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 3b6b453ac51..5092e824e65 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter a").bind "click", (event) =>
+ $(".event-filter-link").on "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,18 +12,10 @@ class @Activities
toggleFilter: (sender) ->
- sender.closest('li').toggleClass "active"
+ $('.event-filter .active').removeClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
- if event_filters
- event_filters = event_filters.split(",")
- else
- event_filters = new Array()
+ $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' }
- index = event_filters.indexOf(filter)
- if index is -1
- event_filters.push filter
- else
- event_filters.splice index, 1
-
- $.cookie "event_filter", event_filters.join(","), { path: '/' }
+ if event_filters isnt filter
+ sender.closest('li').toggleClass "active"
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 367bd098bfd..5463397f475 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -44,6 +44,7 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
+#= require cropper.js
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -210,7 +211,7 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
return
-
+
$(document)
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
@@ -253,7 +254,7 @@ $ ->
$('.page-with-sidebar')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded')
- $.cookie("collapsed_gutter",
+ $.cookie("collapsed_gutter",
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee
index 5d3fe81da74..28f8e103664 100644
--- a/app/assets/javascripts/autosave.js.coffee
+++ b/app/assets/javascripts/autosave.js.coffee
@@ -16,11 +16,11 @@ class @Autosave
try
text = window.localStorage.getItem @key
- catch
+ catch e
return
@field.val text if text?.length > 0
- @field.trigger "input"
+ @field.trigger "input"
save: ->
return unless window.localStorage?
@@ -35,5 +35,5 @@ class @Autosave
reset: ->
return unless window.localStorage?
- try
+ try
window.localStorage.removeItem @key
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 360acb864f6..8f89d3e61a2 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,10 +1,10 @@
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
- $(".add-award").click (event)->
+ $(".add-award").click (event) =>
event.stopPropagation()
event.preventDefault()
- $(".emoji-menu").show()
- $("#emoji_search").focus()
+
+ @showEmojiMenu()
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
@@ -14,6 +14,16 @@ class @AwardsHandler
@renderFrequentlyUsedBlock()
@setupSearch()
+ showEmojiMenu: ->
+ if $(".emoji-menu").length
+ $(".emoji-menu").show()
+ $("#emoji_search").focus()
+ else
+ $.get "/emojis", (response) ->
+ $(".add-award").after response
+ $(".emoji-menu").show()
+ $("#emoji_search").focus()
+
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index b17f8e51470..89f1993797f 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -76,6 +76,8 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
+
+ new TreeView() if $('#tree-slider').length
when 'groups:show'
new Activities()
shortcut_handler = new ShortcutsNavigation()
@@ -88,10 +90,11 @@ class Dispatcher
when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
+ shortcut_handler = new ShortcutsNavigation()
new TreeView()
when 'projects:find_file:show'
shortcut_handler = true
- when 'projects:blob:show'
+ when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 6f569f9e1aa..40cfa59a229 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -70,6 +70,7 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
+ @shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
@@ -185,3 +186,14 @@ class @MergeRequestTabs
expandViewContainer: ->
$('.container-fluid').removeClass('container-limited')
+
+ shrinkView: ->
+ $gutterIcon = $('.gutter-toggle i')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click')
+ , 0)
+
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index 31f6c6d3d47..e6d8518bec8 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -62,6 +62,11 @@ class @Milestone
dataType: "json"
constructor: ->
+ oldMouseStart = $.ui.sortable.prototype._mouseStart
+ $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
+ this._trigger "beforeStart", event, this._uiHash()
+ oldMouseStart.apply this, [event, overrideHandle, noActivation]
+
@bindIssuesSorting()
@bindMergeRequestSorting()
@bindTabsSwitching
@@ -71,6 +76,10 @@ class @Milestone
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
@@ -96,10 +105,22 @@ class @Milestone
).disableSelection()
bindMergeRequestSorting: ->
+ $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
+ currentTabClass = $(e.target).data('show')
+ previousTabClass = $(e.relatedTarget).data('show')
+
+ $(previousTabClass).hide()
+ $(currentTabClass).removeClass('hidden')
+ $(currentTabClass).show()
+
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortMergeRequests(data)
@@ -123,12 +144,3 @@ class @Milestone
Milestone.updateMergeRequest(ui.item, merge_request_url, data)
).disableSelection()
-
- bindMergeRequestSorting: ->
- $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
- currentTabClass = $(e.target).data('show')
- previousTabClass = $(e.relatedTarget).data('show')
-
- $(previousTabClass).hide()
- $(currentTabClass).removeClass('hidden')
- $(currentTabClass).show()
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index bb0b66b86e1..69d590a7533 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -16,11 +16,50 @@ class @Profile
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
- $('.js-choose-user-avatar-button').bind "click", ->
- form = $(this).closest("form")
- form.find(".js-user-avatar-input").click()
+ # 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-user-avatar-input').bind "change", ->
+ $('.js-upload-user-avatar').on 'click', ->
+ $('.edit_user').submit()
+
+ $avatarInput.on "change", ->
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
- form.find(".js-avatar-filename").text(filename)
+ $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])
+
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
index cefa1857d7f..bbf02f1db24 100644
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -24,6 +24,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@nextIssue()
return false
)
+ Mousetrap.bind('e', =>
+ @editIssue()
+ return false
+ )
if isMergeRequest
@@ -63,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation
# Focus the input field
replyField.focus()
+
+ editIssue: ->
+ $editBtn = $('.issuable-edit')
+ Turbolinks.visit($editBtn.attr('href'))
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ae59480af9e..cff309c5972 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -8,4 +8,10 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
+
+ setTimeout ( ->
+ niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars.updateScrollBar();
+ ), 300
+
)
diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 19420f42468..1ee827f1fa3 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -2,7 +2,7 @@
class @Wikis
constructor: ->
- $('.build-new-wiki').bind 'click', (e) =>
+ $('.new-wiki-page').on 'submit', (e) =>
$('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
slug = @slugify(field.val())
@@ -10,6 +10,7 @@ class @Wikis
if (slug.length > 0)
path = field.attr('data-wikis-path')
location.href = path + '/' + slug
+ e.preventDefault()
dasherize: (value) ->
value.replace(/[_\s]+/g, '-')
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0c0451fe4dd..f51054f13dc 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,6 +9,7 @@
*= 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 bd89cc7dc1d..d7e4153ddc0 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -66,7 +66,7 @@
}
.oneline {
- line-height: 42px;
+ line-height: 35px;
}
> p:last-child {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 5f193fa7434..50aa170d24c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -7,7 +7,7 @@
&:focus,
&:active {
outline: none;
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
}
}
@@ -28,7 +28,7 @@
}
&:active {
- @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow ($gl-btn-active-background);
background-color: $dark;
border-color: $border-dark;
@@ -68,6 +68,12 @@
@include btn-default;
@include btn-white;
+ color: $gl-text-color;
+
+ &:focus:active {
+ outline: 0;
+ }
+
&.btn-small,
&.btn-sm {
padding: 4px 10px;
@@ -130,6 +136,11 @@
&.disabled {
pointer-events: auto !important;
}
+
+ .caret {
+ margin-left: 5px;
+ color: $gray-darkest;
+ }
}
.btn-block {
@@ -179,7 +190,7 @@
}
.active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ea56d9e12a0..9ecb547b64f 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -56,6 +56,10 @@ hr {
margin: $gl-padding 0;
}
+.dropdown-menu {
+ margin: 6px 0 0;
+}
+
.dropdown-menu > li > a {
text-shadow: none;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c7f3604850d..07907e6e5a6 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -158,7 +158,7 @@
}
&:hover {
- background: $hover;
+ background: $row-hover;
}
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b7638c86bfa..eab41628677 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -21,10 +21,3 @@
}
}
}
-
-.issues-filters,
-.issues_bulk_update {
- .select2-container .select2-choice {
- color: #444 !important;
- }
-}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a81e258573d..e624982c5c9 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -77,6 +77,7 @@ header {
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
+ overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
@@ -141,9 +142,13 @@ header {
}
@media (max-width: $screen-md-max) {
- .header-collapsed, .header-expanded {
- @include collapsed-header;
+ .header-collapsed {
+ margin-left: $sidebar_collapsed_width;
}
+
+ .header-expanded {
+ margin-left: $sidebar_width;
+ }
}
@media(min-width: $screen-md-max) {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 9854df4c45c..12e2f00fe89 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -44,8 +44,10 @@
white-space: nowrap;
i {
+ float: left;
+ margin-top: 3px;
+ margin-right: 5px;
visibility: hidden;
- @extend .pull-left;
}
&:hover i {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 08dcb563dce..5d7fd36be16 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -6,31 +6,28 @@
.status-box {
@include border-radius(3px);
-
display: block;
float: left;
padding: 0 $gl-btn-padding;
- font-weight: normal;
+ margin-top: 5px;
margin-right: 10px;
+ color: #FFF;
font-size: $gl-font-size;
+ line-height: 25px;
&.status-box-closed {
background-color: $gl-danger;
- color: #FFF;
}
&.status-box-merged {
background-color: $gl-primary;
- color: #FFF;
}
&.status-box-open {
background-color: $green-light;
- color: #FFF;
}
&.status-box-expired {
background: #cea61b;
- color: #FFF;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index d6cd78813c0..0cdcd923b3c 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -48,8 +48,8 @@
.ui-state-hover,
.ui-state-focus {
- border: 1px solid $hover;
- background: $hover;
+ border: 1px solid $row-hover;
+ background: $row-hover;
color: #333;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 5c65383ec1a..b6a781f79de 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -38,7 +38,7 @@
&.smoke { background-color: $background-color; }
&:hover {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
@@ -110,7 +110,20 @@ ul.content-list {
> li {
border-color: $table-border-color;
- color: $gl-gray;
+ color: $list-text-color;
+ font-size: $list-font-size;
+
+ .title {
+ color: $list-title-color;
+ font-weight: 600;
+ }
+
+ .description {
+ p {
+ @include str-truncated;
+ margin-bottom: 0;
+ }
+ }
.avatar {
margin-right: 15px;
@@ -127,13 +140,6 @@ ul.content-list {
}
}
-.panel > .content-list {
- li {
- margin: 0;
- padding: $gl-padding;
- }
-}
-
ul.controls {
padding-top: 1px;
float: right;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1d5000fe388..368bbfe5355 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -41,6 +41,12 @@
transition: $transition;
}
+@mixin transform($transform) {
+ -webkit-transform: $transform;
+ -ms-transform: $transform;
+ transform: $transform;
+}
+
/**
* Prefilled mixins
* Mixins with fixed values
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 252a586358c..d24faa897a1 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -77,12 +77,21 @@
margin-bottom: 0px;
> .dropdown {
- margin-right: 10px;
+ margin-right: $gl-padding-top;
display: inline-block;
}
> .btn {
+ margin-right: $gl-padding-top;
display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn-grouped {
+ float: none;
}
> form {
@@ -94,7 +103,7 @@
display: inline-block;
position: relative;
top: 1px;
- margin-right: 10px;
+ margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 3ee3443e349..7bf04e4ad74 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,49 +1,53 @@
/** Select2 selectbox style override **/
+.select2-container {
+ width: 100% !important;
+}
+
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
- background: #FFF;
- border-color: #DDD;
- height: 36px;
- padding: 6px $gl-padding;
+ background: #fff;
+ border-color: $input-border;
+ border-color: $border-white-light;
+ height: 35px;
+ padding: $gl-vert-padding $gl-btn-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
.select2-arrow {
- background: #FFF;
- border-left: none;
- padding-top: 5px;
+ background-image: none;
+ background-color: transparent;
+ border: none;
+ padding-top: 6px;
+ padding-right: 10px;
+
+ b {
+ @extend .caret;
+ color: $gray-darkest;
+ }
}
.select2-chosen {
- color: $gl-text-color;
+ margin-right: 15px;
}
- &.select2-default {
- .select2-chosen {
- color: #999;
- }
+ &:hover {
+ background-color: $gray-dark;
+ border-color: $border-white-normal;
+ color: $gl-text-color;
}
}
}
-.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{
- color: #7f8fa4;
- border: 1px solid #e7e9ed;
-}
-
-
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- padding: 16px;
- border: none !important;
+ @include border-radius ($border-radius-default);
+ border: none;
}
.select2-results .select2-result-label {
- padding: 9px;
+ padding: 10px 15px;
}
.select2-drop{
@@ -56,15 +60,30 @@
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: 600;
- color: #313236;
+ color: $gl-text-color;
+}
+
+.select2-container-active {
+ .select2-choice, .select2-choices {
+ @include box-shadow(none);
+ }
+}
+
+.select2-dropdown-open {
+ .select2-choice {
+ border-color: $border-white-normal;
+ outline: 0;
+ background-image: none;
+ background-color: $white-dark;
+ @include box-shadow($gl-btn-active-gradient);
+ }
}
.select2-container-multi {
.select2-choices {
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
border-color: $input-border;
- background: white;
- padding-left: $gl-padding / 2;
+ background: none;
.select2-search-field input {
padding: $gl-padding / 2;
@@ -76,14 +95,16 @@
.select2-search-choice {
margin: 8px 0 0 8px;
- background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
+ background-color: $background-color;
+ background-image: none;
.select2-search-choice-close {
- top: 5px;
+ top: 4px;
+ left: 3px;
}
&.select2-search-choice-focus {
@@ -91,22 +112,25 @@
}
}
}
+
+ &.select2-container-active .select2-choices,
+ &.select2-dropdown-open .select2-choices {
+ border-color: $border-white-normal;
+ @include box-shadow($gl-btn-active-gradient);
+ }
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
}
.select2-drop-active {
- border: 1px solid #BBB !important;
- margin-top: 4px;
- font-size: 13px;
+ margin-top: 6px;
+ font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
- .select2-search input {
- background: #fafafa;
- border-color: #DDD;
- }
-
.select2-results {
max-height: 350px;
.select2-highlighted {
@@ -115,8 +139,34 @@
}
}
-.select2-container {
- width: 100% !important;
+.select2-search {
+ padding: 15px 15px 5px;
+
+ .select2-drop-auto-width & {
+ padding: 15px 15px 5px;
+ }
+}
+
+.select2-search input {
+ padding: 2px 25px 2px 5px;
+ background: #fff image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0px 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);
+
+ &:focus {
+ border-color: $input-border-focus;
+ }
+}
+
+.select2-search input.select2-active {
+ background-color: #fff;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
}
/** Branch/tag selector **/
@@ -124,10 +174,19 @@
width: 160px !important;
}
-.ajax-users-dropdown, .ajax-project-users-dropdown {
- .select2-search {
- padding-top: 2px;
- }
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: $gray-light;
+ display: list-item;
+ padding: 10px 15px;
+}
+
+
+.select2-results {
+ margin: 0;
+ padding: 10px 0;
}
.ajax-users-select {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index b141928f706..e0ccd6f100f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -12,6 +12,10 @@
height: 100%;
transition-duration: .3s;
}
+
+ &.right-sidebar-expanded {
+ padding-right: $gutter_width;
+ }
}
.sidebar-wrapper {
@@ -45,19 +49,6 @@
overflow: hidden;
transition-duration: .3s;
- .home {
- z-index: 1;
- position: absolute;
- left: 0px;
- }
-
- #logo {
- z-index: 2;
- position: absolute;
- width: 58px;
- cursor: pointer;
- }
-
a {
float: left;
height: $header-height;
@@ -83,7 +74,7 @@
width: 158px;
float: left;
margin: 0;
- margin-left: 50px;
+ margin-left: 14px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
@@ -194,6 +185,10 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
+ &.right-sidebar-collapsed {
+ padding-right: $sidebar_collapsed_width;
+ }
+
.sidebar-wrapper {
width: $sidebar_width;
@@ -213,17 +208,13 @@
}
}
-@mixin expanded-gutter {
- padding-right: $gutter_width;
-}
-
-@mixin collapsed-gutter {
- padding-right: $sidebar_collapsed_width;
-}
-
@mixin collapsed-sidebar {
padding-left: $sidebar_collapsed_width;
+ &.right-sidebar-collapsed {
+ padding-right: $sidebar_collapsed_width;
+ }
+
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -287,47 +278,10 @@
background: #f2f6f7;
}
-// page is small enough
-@media (max-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
- @include collapsed-sidebar;
- }
-
- .page-gutter {
- &.right-sidebar-collapsed {
- @include collapsed-gutter;
- }
- &.right-sidebar-expanded {
- @include expanded-gutter;
- }
- }
-
- .collapse-nav {
- display: none;
- }
+.page-sidebar-collapsed {
+ @include collapsed-sidebar;
}
-// page is large enough
-@media(min-width: $screen-md-max) {
-
- .page-gutter {
- &.right-sidebar-collapsed {
- @include collapsed-gutter;
- }
- &.right-sidebar-expanded {
- @include expanded-gutter;
- }
- }
-
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
- @include expanded-sidebar;
- }
-} \ No newline at end of file
+.page-sidebar-expanded {
+ @include expanded-sidebar;
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 47b843e5e3d..aa244fe548d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,13 +5,13 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding 0;
+ padding: $gl-padding $gl-btn-padding;
border-color: $table-border-color;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
&:target {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 33270388e64..b1b8295411b 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -70,7 +70,7 @@ $pagination-bg: #fff;
$pagination-border: $border-color;
$pagination-hover-color: $gl-gray;
-$pagination-hover-bg: $hover;
+$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b8386362637..2706d031d7b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,4 +1,4 @@
-$hover: #faf9f9;
+$row-hover: #f4f8fe;
$gl-text-color: #54565B;
$gl-text-green: #4A2;
$gl-text-red: #D12F19;
@@ -31,6 +31,9 @@ $gl-padding-top:10px;
$gl-avatar-size: 40px;
$secondary-text: #7f8fa4;
$error-exclamation-point: #E62958;
+$border-radius-default: 3px;
+$list-title-color: #333333;
+$list-text-color: #555555;
/*
* Color schema
@@ -100,6 +103,8 @@ $gl-success: $green-normal;
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
+$gl-btn-active-background: rgba(0, 0, 0, 0.12);
+$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background;
/*
* Commit Diff Colors
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
new file mode 100644
index 00000000000..e2070f17c3b
--- /dev/null
+++ b/app/assets/stylesheets/pages/appearances.scss
@@ -0,0 +1,11 @@
+.appearance-logo-preview {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
+.appearance-light-logo-preview {
+ background-color: $background-color;
+ max-width: 72px;
+ padding: 10px;
+ margin-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ef62f069dc2..b61d1f180b3 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -64,7 +64,6 @@
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
- overflow-x: hidden;
// --
&:first-child {
@@ -90,7 +89,6 @@
.gutter-toggle {
margin-left: 20px;
- border-left: 1px solid $border-gray-light;
padding-left: 10px;
&:hover {
@@ -157,11 +155,10 @@
.right-sidebar {
position: fixed;
top: 58px;
+ bottom: 0;
right: 0;
- height: 100%;
- transition-duration: .3s;
+ transition: width .3s;
background: $gray-light;
- overflow: scroll;
padding: 10px 20px;
&.right-sidebar-expanded {
@@ -170,6 +167,14 @@
hr {
display: none;
}
+
+ .sidebar-collapsed-icon {
+ display: none;
+ }
+
+ .gutter-toggle {
+ border-left: 1px solid $border-gray-light;
+ }
}
.subscribe-button {
@@ -181,7 +186,6 @@
&.right-sidebar-collapsed {
width: $sidebar_collapsed_width;
padding-top: 0;
- overflow-x: hidden;
hr {
margin: 0;
@@ -192,42 +196,25 @@
}
.block {
- border-bottom: none;
+ width: $sidebar_collapsed_width - 1px;
+ margin-left: -19px;
padding: 15px 0 0 0;
+ border-bottom: none;
+ overflow: hidden;
}
- }
-
- .btn {
- background: $gray-normal;
- border: 1px solid $border-gray-normal;
- &:hover {
- background: $gray-dark;
- border: 1px solid $border-gray-dark;
- }
- }
- &.right-sidebar-collapsed {
- .issuable-count,
- .issuable-nav,
- .assignee > *,
- .milestone > *,
- .labels > *,
- .participants > *,
- .light > *,
- .project-reference > * {
+ .hide-collapsed {
display: none;
}
.gutter-toggle {
- margin-left: -$gutter_inner_width + 4;
+ margin-left: -36px;
}
.sidebar-collapsed-icon {
display: block;
- float: left;
- width: 62px;
+ width: 100%;
text-align: center;
- margin-left: -19px;
padding-bottom: 10px;
color: #999999;
@@ -247,14 +234,15 @@
color: #999999;
}
}
-
}
-
}
- &.right-sidebar-expanded {
- .sidebar-collapsed-icon {
- display: none;
+ .btn {
+ background: $gray-normal;
+ border: 1px solid $border-gray-normal;
+ &:hover {
+ background: $gray-dark;
+ border: 1px solid $border-gray-dark;
}
}
}
@@ -263,4 +251,4 @@
small {
color: $gray-darkest;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8694bd654a7..a2ca00234ed 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -4,13 +4,7 @@
position: relative;
.issue-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: 600;
- }
-
- .issue-info {
- color: $gl-gray;
+ margin-bottom: 2px;
}
.issue-check {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 9a2c4b83ffb..2772623f4bd 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -148,15 +148,8 @@
position: relative;
.merge-request-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: 600;
- }
-
- .merge-request-info {
- color: $gl-gray;
+ margin-bottom: 2px;
}
-
}
.merge-request-labels {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 95fc26a608a..de4d9fd80fa 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -51,9 +51,17 @@
.profile-link-holder {
display: inline;
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
+
+// Middle dot divider between each element in a list of items.
+.middle-dot-divider {
&:after {
- content: "\00B7";
- padding: 0px 6px;
+ content: "\00B7"; // Middle Dot
+ padding: 0 6px;
font-weight: bold;
}
@@ -63,9 +71,46 @@
padding: 0;
}
}
+}
- a {
- color: $blue-dark;
- text-decoration: none;
+.profile-user-bio {
+ // Limits the width of the user bio for readability.
+ max-width: 750px;
+ margin: auto;
+}
+
+.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;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 542ac896f6b..247ac83c24a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -32,6 +32,7 @@
.cover-controls {
.project-settings-dropdown {
margin-left: 10px;
+ display: inline-block;
}
}
@@ -186,10 +187,10 @@
.dropdown-menu {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
+ @include border-radius ($border-radius-default);
border: none;
- padding: 16px 0;
+ padding: 10px 0;
font-size: 14px;
font-weight: 100;
@@ -396,15 +397,10 @@ pre.light-well {
.project-full-name {
@include str-truncated;
- font-weight: 600;
- color: #4c4e54;
}
- .project-controls {
- float: right;
- color: $gl-gray;
+ .controls {
line-height: 40px;
- color: #7f8fa4;
a:hover {
text-decoration: none;
@@ -414,16 +410,6 @@ pre.light-well {
margin-left: 10px;
}
}
-
- .project-description {
- color: #7f8fa4;
-
- p {
- @include str-truncated;
- margin-bottom: 0;
- color: #7f8fa4;
- }
- }
}
.bottom {
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 1430d01859d..0161642d871 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,30 +2,6 @@
padding: 2px;
}
-
-.snippet-row {
- .snippet-title {
- font-size: 15px;
- font-weight: bold;
- line-height: 20px;
- margin-bottom: 2px;
-
- .monospace {
- font-weight: normal;
- }
- }
-
- .snippet-info {
- color: #888;
- font-size: 13px;
- line-height: 24px;
-
- a {
- color: #888;
- }
- }
-}
-
.snippet-holder {
margin-bottom: -$gl-padding;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 2f57f21963d..0dc5a905f99 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -12,29 +12,10 @@
}
}
-.todos {
- .panel {
- border-top: none;
- margin-bottom: 0;
- }
-}
-
.todo-item {
font-size: $gl-font-size;
- padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
- border-bottom: 1px solid $table-border-color;
- color: #7f8fa4;
-
- &.todo-inline {
- .avatar {
- position: relative;
- top: -2px;
- }
-
- .todo-title {
- line-height: 40px;
- }
- }
+ padding-left: $gl-avatar-size + $gl-padding-top;
+ color: $secondary-text;
a {
color: #4c4e54;
@@ -48,7 +29,7 @@
@include str-truncated(calc(100% - 174px));
font-weight: 600;
- .author_name {
+ .author-name {
color: #333;
}
}
@@ -88,17 +69,7 @@
margin-bottom: 0;
}
}
-
- .todo-note-icon {
- color: #777;
- float: left;
- font-size: $gl-font-size;
- line-height: 16px;
- margin-right: 5px;
- }
}
-
- &:last-child { border:none }
}
@media (max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index c7411617cb3..ef63b010600 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -21,7 +21,7 @@
&:hover {
td {
- background: $hover;
+ background: $row-hover;
}
cursor: pointer;
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 185f3622e64..05fa9312efb 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -3,4 +3,15 @@
margin: 35px 0 20px;
font-weight: bold;
}
+
+ .example {
+ &:before {
+ content: "Example";
+ color: #BBB;
+ }
+
+ padding: 15px;
+ border: 1px dashed #ddd;
+ margin-bottom: 15px;
+ }
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index cdf514197cb..dfaeba41cf6 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,8 +4,3 @@
margin-right: auto;
padding-right: 7px;
}
-
-.wiki-last-edit-by {
- font-size: 80%;
- font-weight: normal;
-}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
new file mode 100644
index 00000000000..26cf74e4849
--- /dev/null
+++ b/app/controllers/admin/appearances_controller.rb
@@ -0,0 +1,57 @@
+class Admin::AppearancesController < Admin::ApplicationController
+ before_action :set_appearance, except: :create
+
+ def show
+ end
+
+ def preview
+ end
+
+ def create
+ @appearance = Appearance.new(appearance_params)
+
+ if @appearance.save
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully created.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def update
+ if @appearance.update(appearance_params)
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def logo
+ @appearance.remove_logo!
+
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ end
+
+ def header_logos
+ @appearance.remove_header_logo!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_appearance
+ @appearance = Appearance.last || Appearance.new
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def appearance_params
+ params.require(:appearance).permit(
+ :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
+ :updated_by
+ )
+ end
+end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 9ee9039f004..43cf8fa71af 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -15,7 +15,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy_all
- @todos.each(&:done)
+ @todos.each(&:done!)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
new file mode 100644
index 00000000000..1bec5a7d27f
--- /dev/null
+++ b/app/controllers/emojis_controller.rb
@@ -0,0 +1,6 @@
+class EmojisController < ApplicationController
+ layout false
+
+ def index
+ end
+end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 28803164fcf..fa7a1148961 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -65,6 +65,9 @@ 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/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index a8f091819ca..00df1c9c965 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -64,9 +64,9 @@ class Projects::RefsController < Projects::ApplicationController
}
end
- if @logs.present?
- @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))
- @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))
+ offset = (@offset + @limit)
+ if contents.size > offset
+ @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
respond_to do |format|
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 868b05929d7..509f4f412ca 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -55,14 +55,15 @@ class UploadsController < ApplicationController
"user" => User,
"project" => Project,
"note" => Note,
- "group" => Group
+ "group" => Group,
+ "appearance" => Appearance
}
upload_models[params[:model]]
end
def upload_mount
- upload_mounts = %w(avatar attachment file)
+ upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c5820bf4c50..e0abc3a2869 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,21 +1,33 @@
module AppearancesHelper
- def brand_item
- nil
- end
-
def brand_title
- 'GitLab Community Edition'
+ if brand_item && brand_item.title
+ brand_item.title
+ else
+ 'GitLab Community Edition'
+ end
end
def brand_image
- nil
+ if brand_item.logo?
+ image_tag brand_item.logo
+ else
+ nil
+ end
end
def brand_text
- nil
+ markdown(brand_item.description)
+ end
+
+ def brand_item
+ @appearance ||= Appearance.first
end
def brand_header_logo
- render 'shared/logo.svg'
+ if brand_item && brand_item.header_logo?
+ image_tag brand_item.header_logo
+ else
+ render 'shared/logo.svg'
+ end
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index de669e529a7..b4f80fd9b3e 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -6,6 +6,10 @@ module AuthHelper
Gitlab.config.ldap.enabled
end
+ def omniauth_enabled?
+ Gitlab.config.omniauth.enabled
+ end
+
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7ff539118d3..a09e91578b6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -130,7 +130,7 @@ module CommitsHelper
if can_collaborate_with_project?
content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
end
elsif can?(current_user, :fork_project, @project)
continue_params = {
@@ -142,7 +142,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
- link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', title: tooltip
+ link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end
end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
new file mode 100644
index 00000000000..4cf8dd9a8ce
--- /dev/null
+++ b/app/models/appearance.rb
@@ -0,0 +1,9 @@
+class Appearance < ActiveRecord::Base
+ validates :title, presence: true
+ validates :description, presence: true
+ validates :logo, file_size: { maximum: 1.megabyte }
+ validates :header_logo, file_size: { maximum: 1.megabyte }
+
+ mount_uploader :logo, AttachmentUploader
+ mount_uploader :header_logo, AttachmentUploader
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b99abb540ea..3224f5457f0 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -232,7 +232,7 @@ class Commit
end
def reverts_commit?(commit)
- description.include?(commit.revert_description)
+ description? && description.include?(commit.revert_description)
end
def merge_commit?
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index cbe65d70997..8f99e3bef9b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -27,7 +27,7 @@ class Milestone < ActiveRecord::Base
belongs_to :project
has_many :issues
- has_many :labels, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :participants, through: :issues, source: :assignee
diff --git a/app/models/project.rb b/app/models/project.rb
index 95ad88c76ae..6f5d592755a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -801,10 +801,7 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
- # Cached divergent commit counts are based on repository head
- repository.expire_branch_cache
- repository.expire_root_ref_cache
-
+ repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e050bd45254..a214a69d749 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -245,15 +245,6 @@ class Repository
expire_emptiness_caches if empty?
end
- # Expires _all_ caches, including those that would normally only be expired
- # under specific conditions.
- def expire_all_caches!
- expire_cache
- expire_root_ref_cache
- expire_emptiness_caches
- expire_has_visible_content_cache
- end
-
def expire_branch_cache(branch_name = nil)
# When we push to the root branch we have to flush the cache for all other
# branches as their statistics are based on the commits relative to the
@@ -307,6 +298,46 @@ class Repository
cache.expire(:branch_names)
end
+ # Runs code just before a repository is deleted.
+ def before_delete
+ expire_cache if exists?
+
+ expire_root_ref_cache
+ expire_emptiness_caches
+ end
+
+ # Runs code just before the HEAD of a repository is changed.
+ def before_change_head
+ # Cached divergent commit counts are based on repository head
+ expire_branch_cache
+ expire_root_ref_cache
+ end
+
+ # Runs code before creating a new tag.
+ def before_create_tag
+ expire_cache
+ end
+
+ # Runs code after a repository has been forked/imported.
+ def after_import
+ expire_emptiness_caches
+ end
+
+ # Runs code after a new commit has been pushed.
+ def after_push_commit(branch_name)
+ expire_cache(branch_name)
+ end
+
+ # Runs code after a new branch has been created.
+ def after_create_branch
+ expire_has_visible_content_cache
+ end
+
+ # Runs code after an existing branch has been removed.
+ def after_remove_branch
+ expire_has_visible_content_cache
+ end
+
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 34d71c1b0d3..5f91991f781 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -36,7 +36,7 @@ class Todo < ActiveRecord::Base
state_machine :state, initial: :pending do
event :done do
- transition pending: :done
+ transition [:pending, :done] => :done
end
state :pending
diff --git a/app/models/user.rb b/app/models/user.rb
index 02ff2456f2b..6baf2468ade 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -98,6 +98,9 @@ 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
#
@@ -163,6 +166,11 @@ 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? }
+
before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index a1711d234ff..9ba200f7bde 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -16,13 +16,13 @@ class GitPushService < BaseService
# 5. Executes the project's services
#
def execute
- @project.repository.expire_cache(branch_name)
+ @project.repository.after_push_commit(branch_name)
if push_remove_branch?
- @project.repository.expire_has_visible_content_cache
+ @project.repository.after_remove_branch
@push_commits = []
elsif push_to_new_branch?
- @project.repository.expire_has_visible_content_cache
+ @project.repository.after_create_branch
# Re-find the pushed commits.
if is_default_branch?
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 4144c7111d0..a62c5fc4fc4 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
- project.repository.expire_cache
+ project.repository.before_create_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 531bbc9b067..d6af12f9739 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -24,10 +24,14 @@ module MergeRequests
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
+ next unless merge_request.mergeable?
- if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
- end
+ ci_commit = merge_request.ci_commit
+ next unless ci_commit
+ next unless ci_commit.sha == commit_status.sha
+ next unless ci_commit.success?
+
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
@@ -51,6 +55,8 @@ module MergeRequests
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
+ return [] if branches.blank?
+
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index b970439b921..2bb312bb252 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -13,6 +13,5 @@ module Notes
note
end
-
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index f4dcb142850..df5054f08d7 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -76,11 +76,9 @@ module Projects
end
def flush_caches(project, wiki_path)
- project.repository.expire_all_caches! if project.repository.exists?
+ project.repository.before_delete
- wiki_repo = Repository.new(wiki_path, project)
-
- wiki_repo.expire_all_caches! if wiki_repo.exists?
+ Repository.new(wiki_path, project).before_delete
end
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index dc270602ebc..4392e2d17fe 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -130,8 +130,8 @@ class TodoService
end
def handle_note(note, author)
- # Skip system notes, like status changes and cross-references
- return if note.system
+ # Skip system notes, notes on commit, and notes on project snippet
+ return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type)
project = note.project
target = note.noteable
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 6135c3ad96f..2c72df44ff0 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -2,11 +2,22 @@
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/validators/url_validator.rb b/app/validators/url_validator.rb
index 2848b9cd33d..a77beb2683d 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -29,8 +29,11 @@ class UrlValidator < ActiveModel::EachValidator
end
def valid_url?(value)
+ return false if value.nil?
+
options = default_options.merge(self.options)
+ value.strip!
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
new file mode 100644
index 00000000000..6f325914d14
--- /dev/null
+++ b/app/views/admin/appearances/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+ - if @appearance.errors.any?
+ .alert.alert-danger
+ - @appearance.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset.sign-in
+ %legend
+ Sign in/Sign up pages:
+ .form-group
+ = f.label :title, class: 'control-label'
+ .col-sm-10
+ = f.text_field :title, class: "form-control"
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_area :description, class: "form-control", rows: 10
+ .hint
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
+ .form-group
+ = f.label :logo, class: 'control-label'
+ .col-sm-10
+ - if @appearance.logo?
+ = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ - if @appearance.persisted?
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+
+ - if @appearance.updated_at
+ %span.pull-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
new file mode 100644
index 00000000000..dd4a64e80bc
--- /dev/null
+++ b/app/views/admin/appearances/preview.html.haml
@@ -0,0 +1,29 @@
+- page_title "Preview | Appearance"
+%h3.page-title
+ Appearance settings - Preview
+%hr
+
+.ui-box
+ .title
+ Sign-in page
+ %div
+ .login-page
+ .container
+ .content
+ .login-title
+ %h1= brand_title
+ %hr
+ .container
+ .content
+ .row
+ .col-sm-7
+ .brand-image
+ = brand_image
+ .brand_text
+ = brand_text
+ .col-sm-4
+ .login-box
+ %h3.page-title Sign in
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
new file mode 100644
index 00000000000..089e8e4cb7a
--- /dev/null
+++ b/app/views/admin/appearances/show.html.haml
@@ -0,0 +1,7 @@
+- page_title "Appearance"
+%h3.page-title
+ Appearance settings
+%p.light
+ You can modify the look and feel of GitLab here
+
+= render 'form'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 6975f6ed0db..f878d36e739 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,11 +1,11 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
- .todo-item{class: 'todo-block'}
+ .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title
- %span.author_name
+ %span.author-name
= link_to_author todo
- %span.todo_label
+ %span.todo-label
= todo_action_name(todo)
= todo_target_link(todo)
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dbc8eda6196..d65fa60025c 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,10 +1,10 @@
- page_title "Sign in"
%div
- - if signin_enabled? || ldap_enabled?
+ - if signin_enabled? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
+ - if omniauth_enabled? && devise_mapping.omniauthable?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
@@ -14,6 +14,6 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
+ - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
new file mode 100644
index 00000000000..b66e513e4d2
--- /dev/null
+++ b/app/views/emojis/index.html.haml
@@ -0,0 +1,9 @@
+.emoji-menu
+ .emoji-menu-content
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
+ - AwardEmoji.emoji_by_category.each do |category, emojis|
+ %h5= AwardEmoji::CATEGORIES[category]
+ %ul
+ - emojis.each do |emoji|
+ %li
+ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) \ No newline at end of file
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 9c16ab7e30f..209729dc7ee 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,11 +1,12 @@
.top-area
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'input-short project-filter-form-field form-control projects-list-filter', spellcheck: false, id: 'project-filter-form-field'
- - if current_user && current_user.can_create_project?
- = link_to new_project_path, class: 'btn btn-new' do
- = icon('plus')
- New Project
+ - if @projects.present?
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ - 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
.projects-list-holder
= render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a0ba11b11a1..6148d8cb3d2 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -32,10 +32,9 @@
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- - if @projects.present?
- %li
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
+ %li
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ Projects
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 8e982718d23..82d2d4aabed 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -229,6 +229,10 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
@@ -245,3 +249,7 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 746386cab58..a2c0a858930 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -31,64 +31,91 @@
%h2#blocks Blocks
- %h4
+ .lead
+ Content block separated with botton border
+ %code .content-block
+
+ .example
+ .content-block
+ %h4 Normal block inside content
+ = lorem
+
+ .content-block
+ %h4 Second block
+ = lorem
+
+ .lead
+ Gray content block with side padding using
%code .gray-content-block
- .gray-content-block.middle-block
- %h4 Normal block inside content
- = lorem
+ .example
+ .gray-content-block
+ %h4 Normal block inside content
+ = lorem
- .gray-content-block.second-block
- %h4 Second block
- = lorem
+ .gray-content-block.second-block
+ %h4 Second block
+ = lorem
- %h4
+ .lead
+ Cover block for profile page with avatar, name and description
%code .cover-block
- %br
- .cover-block
- .avatar-holder
- = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
- .cover-title
- John Smith
-
- .cover-desc
- = lorem
+ .example
+ .cover-block
+ .avatar-holder
+ = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
+ .cover-title
+ John Smith
+
+ .cover-desc
+ = lorem
- .cover-controls
- = link_to '#', class: 'btn btn-gray' do
- = icon('pencil')
- &nbsp;
- = link_to '#', class: 'btn btn-gray' do
- = icon('rss')
+ .cover-controls
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('pencil')
+ &nbsp;
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('rss')
%h2#lists Lists
- %h4
+ .lead
+ Simple list using
%code .content-list
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
- %h4
- %code .well-list
- %ul.well-list
- %li
- One item
- %li
- One item
- %li
- One item
+ .example
+ %ul.content-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
- %h4
- %code .panel .well-list
+ .lead
+ List with avatar, title and description using
+ %code .content-list
+
+ .example
+ %ul.content-list
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
- .panel.panel-default
- .panel-heading Your list
+ .lead
+ List with hover effect
+ %code .well-list
+ .example
%ul.well-list
%li
One item
@@ -97,17 +124,18 @@
%li
One item
- %h4
- %code .bordered-list
- %ul.bordered-list
- %li
- One item
- %li
- One item
- %li
- One item
-
-
+ .lead
+ List inside panel
+ .example
+ .panel.panel-default
+ .panel-heading Your list
+ %ul.well-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
%h2#tables Tables
@@ -138,9 +166,9 @@
%h2#navs Navigation
- %h4
+ .lead
+ Holder for top page navigation. Includes navigation, search field, sorting and button
%code .top-area
- %p Holder for top page navigation. Includes navigation, search field, sorting and button
.example
.top-area
@@ -161,9 +189,9 @@
= link_to 'New issue', '#', class: 'btn btn-new'
- %h4
+ .lead
+ Only nav links without button and search
%code .nav-links
- %p Only nav links without button and search
.example
%ul.nav-links
%li.active
@@ -228,43 +256,47 @@
%h2#forms Forms
- %h4
+ .lead
+ Horizontal form when label rendered inline with input
%code form.horizontal-form
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
- .col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
- .form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
- .col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
-
- %h4
+ .example
+ %form.form-horizontal
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ .col-sm-10
+ %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ .col-sm-10
+ %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %button.btn.btn-default{:type => "submit"} Sign in
+
+ .lead
+ Form when label rendered above input
%code form
- %form
- .form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
- .form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ .example
+ %form
+ .form-group
+ %label{:for => "exampleInputEmail1"} Email address
+ %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ .form-group
+ %label{:for => "exampleInputPassword1"} Password
+ %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ %button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
%h4
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 38ca4f91c4d..79cdbac1f37 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -44,6 +44,7 @@
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
+ %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
-# Windows 8 pinned site tile
%meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac1d5429382..280a1b93729 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -56,6 +56,11 @@
= icon('cog fw')
%span
Background Jobs
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ = icon('image')
+ %span
+ Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index 0436c2213da..86f35823406 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -1,7 +1,7 @@
- page_title "Applications"
- header_title page_title, applications_profile_path
-.gray-content-block.top-block
+.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.
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 8fdba45b193..8f45f41cfe3 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,7 +1,7 @@
- page_title "Audit Log"
- header_title page_title, audit_log_profile_path
-.gray-content-block.top-block
+.alert.alert-help.prepend-top-default
History of authentications
.prepend-top-default
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1d140347a5f..705e1804717 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,24 +1,22 @@
- page_title "Emails"
- header_title page_title, profile_emails_path
-.gray-content-block.top-block
- Control emails linked to your account
-
-%ul.prepend-top-default
- %li
- Your
- %b Primary Email
- will be used for avatar detection and web based operations, such as edits and merges.
- %li
- Your
- %b Notification Email
- will be used for account notifications.
- %li
- Your
- %b Public Email
- will be displayed on your public profile.
- %li
- All email addresses will be used to identify your commits.
+.alert.alert-help.prepend-top-default
+ %ul
+ %li
+ Your
+ %b Primary Email
+ will be used for avatar detection and web based operations, such as edits and merges.
+ %li
+ Your
+ %b Notification Email
+ will be used for account notifications.
+ %li
+ Your
+ %b Public Email
+ will be displayed on your public profile.
+ %li
+ All email addresses will be used to identify your commits.
.panel.panel-default
.panel-heading
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 17a4195030e..c9a6a93f545 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,14 +1,14 @@
- page_title "SSH Keys"
- header_title page_title, profile_keys_path
-.gray-content-block.top-block
- .pull-right
+.top-area
+ .nav-text
+ Before you can add an SSH key you need to
+ = link_to "generate it.", help_page_path("ssh", "README")
+ .nav-controls
= link_to new_profile_key_path, class: "btn btn-new" do
= icon('plus')
Add SSH Key
- .oneline
- Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
.prepend-top-default
= render 'key_table'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 0bcadc965fa..d5f61d9f0ca 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,9 +1,6 @@
- page_title "Notifications"
- header_title page_title, profile_notifications_path
-.gray-content-block.top-block
- These are your global notification settings.
-
.prepend-top-default
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
-if @user.errors.any?
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index fab7c45c9b2..ab070c09beb 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,7 +1,7 @@
- page_title "Password"
- header_title page_title, edit_profile_password_path
-.gray-content-block.top-block
+.alert.alert-help.prepend-top-default
- if @user.password_automatically_set?
Set your password.
- else
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 877589dc390..1a53b4393e4 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,8 +1,7 @@
- page_title 'Preferences'
- header_title page_title, profile_preferences_path
-- @blank_container = true
-.alert.alert-help
+.alert.alert-help.prepend-top-default
These settings allow you to customize the appearance and behavior of the site.
They are saved with your account and will persist to any device you use to
access the site.
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index add9a00138b..64c4bdceff9 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.top-block
+.alert.alert-help.prepend-top-default
This information will appear on your profile.
- if current_user.ldap_user?
Some options are unavailable for LDAP accounts
@@ -90,6 +90,9 @@
&nbsp;
%span.file_name.js-avatar-filename File name...
= f.file_field :avatar, class: "js-user-avatar-input hidden"
+ = f.hidden_field :avatar_crop_x
+ = f.hidden_field :avatar_crop_y
+ = f.hidden_field :avatar_crop_size
.light The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
@@ -99,3 +102,19 @@
.form-actions
= f.submit 'Save changes', 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/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index eb6fbfaffa0..5f9a92ff93f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -3,7 +3,7 @@
%h3.page-title Blame view
-#tree-holder.tree-holder
+#blob-content-holder.tree-holder
.file-holder
.file-title
= blob_icon @blob.mode, @blob.name
@@ -33,7 +33,9 @@
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
- %a.diff-line-num= i
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ = icon("link")
+ = i
\
- current_line += line_count
%td.lines
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 5e3bd14565e..14f1d3226bb 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -51,9 +51,11 @@
%th Name
%th Duration
%th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
%th
- @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+ = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 042f660077e..f2e56081afe 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,5 +1,3 @@
-- @blank_container = true
-
.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index b34d106d565..6ad7b05155a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -18,7 +18,7 @@
= link_to "adding README", new_readme_path, class: 'underlined-link'
file to this project.
-- if can?(current_user, :download_code, @project)
+- if can?(current_user, :push_code, @project)
%div{ class: container_class }
.prepend-top-20
.empty_wrapper
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 673020a4e30..eb9c225df2f 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 654d8cd5ed0..a44f34c2a68 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -5,7 +5,7 @@
.issue-title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "row_title"
+ = link_to_gfm issue.title, issue_path(issue), class: "title"
%ul.controls.light
- if issue.closed?
%li
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 69a0e2a0c4d..1173e0a78c7 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -8,12 +8,12 @@
.detail-page-header
.pull-right
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
- New Issue
+ New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b55f6a2d32a..b9d5982a56f 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,7 +1,7 @@
%li{ class: mr_css_classes(merge_request) }
.merge-request-title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
+ = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
%ul.controls.light
- if merge_request.merged?
%li
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index db7f244d002..8ee2aef0e61 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,12 +8,9 @@
row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
-- if @logs.present?
+- if @more_log_url
:plain
- var current_url = location.href.replace(/\/?$/, '/');
- var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/');
-
- if(current_url == log_url) {
+ if($('#tree-slider').length) {
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 29bf5d62abe..2b91b7e8f65 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,12 +1,11 @@
-%span.pull-right
- - if (@page && @page.persisted?)
- = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- Page History
- - if can?(current_user, :create_wiki, @project)
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
- = icon('trash')
- Delete
+- if (@page && @page.persisted?)
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ Page History
+ - if can?(current_user, :create_wiki, @project)
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
+ = icon('trash')
+ Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 56a53ffff2a..a722fbc5352 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -16,4 +16,4 @@
= icon('plus')
New Page
- = render 'projects/wikis/new'
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 53b37b1104e..919daf0a7b2 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,9 +5,10 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- .form-group
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- .form-actions
- = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 23f64fbbd10..4dd818c7f67 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,20 @@
- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- .pull-right
+
+.top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
+
+ .nav-controls
= render 'main_links'
- %h3.page-title.oneline
- %span.light Edit Page
- - if @page.persisted?
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title
= render 'form'
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4322146ce34..dcaddae2b04 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,11 +1,14 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- %h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+
+.top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aae1ad69ad9..92b494a513c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -2,15 +2,12 @@
= render "header_title"
= render 'nav'
-.gray-content-block
- All pages in this wiki are listed below.
-
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
- %h4
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 309d40f52bc..067fb7f8f54 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,17 +1,18 @@
- page_title @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- = render 'main_links'
- %h3.page-title.oneline
- = @page.title.capitalize
+.top-area
+ .nav-text
+ %strong= @page.title.capitalize
%span.wiki-last-edit-by
&middot;
last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ .nav-controls
+ = render 'main_links'
+
- if @page.historical?
.warning_message
This is an old version of this page.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 4b4c9e9eabe..8ff9d4c1c7f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
- %ul.well-list.issues-list
+ %ul.content-list.issues-list
- group[1].each do |issue|
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index be17a511b26..e74fc36c797 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
- %ul.well-list.mr-list
+ %ul.content-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 289b0bfe1eb..fb9a8db0889 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -22,13 +22,13 @@
= number_with_delimiter(group.users.count)
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
- = link_to group, class: 'group-name' do
- %span.item-title= group.name
+ = link_to group, class: 'group-name title' do
+ = group.name
- if group_member
as
%span #{group_member.human_access}
- if group.description.present?
- .light
+ .description
= markdown(group.description, pipeline: :description)
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index ea61935487c..f1d92ef48b2 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -3,7 +3,8 @@
= icon('users')
%span
= participants.count
- .title
+ .title.hide-collapsed
= pluralize participants.count, "participant"
- participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
+ %span.hide-collapsed
+ = link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index a45775f36b5..36f06377886 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,14 +1,14 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
.block
- %span.issuable-count.pull-left
+ %span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
%span.pull-right
%a.gutter-toggle{href: '#'}
= sidebar_gutter_toggle_icon
- .issuable-nav.pull-right.btn-group{role: 'group', "aria-label" => '...'}
+ .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
- else
@@ -27,13 +27,13 @@
= link_to_member_avatar(issuable.assignee, size: 24)
- else
= icon('user')
- .title
+ .title.hide-collapsed
%label
Assignee
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
- .value
+ .value.hide-collapsed
- if issuable.assignee
%strong= link_to_member(@project, issuable.assignee, size: 24)
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
@@ -42,7 +42,7 @@
- else
.light None
- .selectbox
+ .selectbox.hide-collapsed
= users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
.block.milestone
@@ -53,13 +53,13 @@
= issuable.milestone.title
- else
No
- .title
+ .title.hide-collapsed
%label
Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
- .value
+ .value.hide-collapsed
- if issuable.milestone
%span.back-to-milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
@@ -68,7 +68,7 @@
= issuable.milestone.title
- else
.light None
- .selectbox
+ .selectbox.hide-collapsed
= f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
= hidden_field_tag :issuable_context
= f.submit class: 'btn hide'
@@ -79,18 +79,18 @@
= icon('tags')
%span
= issuable.labels.count
- .title
+ .title.hide-collapsed
%label Labels
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels
+ .value.issuable-show-labels.hide-collapsed
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
.light None
- .selectbox
+ .selectbox.hide-collapsed
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
@@ -101,12 +101,12 @@
.block.light
.sidebar-collapsed-icon
= icon('rss')
- .title
+ .title.hide-collapsed
%label.light Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
+ %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status{data: {status: subscribtion_status}}
+ .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )}
@@ -116,8 +116,7 @@
.block.project-reference
.sidebar-collapsed-icon
= clipboard_button(clipboard_text: project_ref)
- .title
- .cross-project-reference
+ .cross-project-reference.hide-collapsed
%span
Reference:
%cite{title: project_ref}
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 75684b972f1..e75af50a537 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -7,7 +7,7 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
-%ul.projects-list
+%ul.projects-list.content-list
- if projects.any?
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 97db5b1d41e..99e48e86e38 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -19,7 +19,7 @@
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- else
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name
+ %span.project-full-name.title
%span.namespace-name
- if project.namespace && !skip_namespace
= project.namespace.human_name
@@ -27,7 +27,7 @@
%span.project-name.filter-title
= project.name
- .project-controls
+ .controls
- if ci_commit
%span
= render_ci_status(ci_commit)
@@ -39,10 +39,13 @@
%span
= icon('star')
= project.star_count
+ %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' },
+ title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
+ = visibility_level_icon(project.visibility_level, fw: false)
- if show_last_commit_as_description
- .project-description
+ .description
= link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
class: "commit-row-message"
- elsif project.description.present?
- .project-description
+ .description
= markdown(project.description, pipeline: :description)
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c6294caddc7..a316a085107 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,10 +1,12 @@
%li.snippet-row
+ = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
+
.snippet-title
- = link_to reliable_snippet_path(snippet) do
+ = link_to reliable_snippet_path(snippet), class: 'title' do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
- %i.fa.fa-lock
+ = icon('lock')
private
%span.monospace.pull-right
= snippet.file_name
@@ -15,6 +17,5 @@
.snippet-info
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index d9aa4dd1d2e..80a3e731e1d 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,4 @@
-%ul.bordered-list
+%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
- if @snippets.empty?
%li
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 3bfd781e51d..d109635fa1e 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -35,35 +35,37 @@
= @user.name
.cover-desc
- %span
- @#{@user.username}.
- - if @user.bio.present?
- %span
- #{@user.bio}.
- %span
+ %span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
Member since #{@user.created_at.to_s(:medium)}
+
+ - if @user.bio.present?
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
.cover-desc
- unless @user.public_email.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= link_to @user.public_email, "mailto:#{@user.public_email}"
- unless @user.skype.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= link_to "skype:#{@user.skype}", title: "Skype" do
= icon('skype')
- unless @user.linkedin.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
= icon('linkedin-square')
- unless @user.twitter.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
= icon('twitter-square')
- unless @user.website_url.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= link_to @user.short_website_url, @user.full_website_url
- unless @user.location.blank?
- .profile-link-holder
+ .profile-link-holder.middle-dot-divider
= icon('map-marker')
= @user.location
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 91c5b7eac5e..176fd29cb57 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -9,15 +9,6 @@
.awards-controls
%a.add-award{"href" => "#"}
= icon('smile-o')
- .emoji-menu
- .emoji-menu-content
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- - AwardEmoji.emoji_by_category.each do |category, emojis|
- %h5= AwardEmoji::CATEGORIES[category]
- %ul
- - emojis.each do |emoji|
- %li
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
- if current_user
:javascript
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 2572b9d6d98..21d311579e3 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -27,7 +27,7 @@ class RepositoryForkWorker
return
end
- project.repository.expire_emptiness_caches
+ project.repository.after_import
project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 0b6f746e118..2937493c614 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -18,7 +18,7 @@ class RepositoryImportWorker
return
end
- project.repository.expire_emptiness_caches
+ project.repository.after_import
project.import_finish
end
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d8170557f7e..713204b1c51 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -131,6 +131,7 @@ Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['au
Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil?
Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil?
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
+Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil?
Settings.omniauth['providers'] ||= []
Settings.omniauth['cas3'] ||= Settingslogic.new({})
diff --git a/config/newrelic.yml b/config/newrelic.yml
new file mode 100644
index 00000000000..9ef922a38d9
--- /dev/null
+++ b/config/newrelic.yml
@@ -0,0 +1,16 @@
+# New Relic configuration file
+#
+# This file is here to make sure the New Relic gem stays
+# quiet by default.
+#
+# To enable and configure New Relic, please use
+# environment variables, e.g. NEW_RELIC_ENABLED=true
+
+production:
+ enabled: false
+
+development:
+ enabled: false
+
+test:
+ enabled: false
diff --git a/config/routes.rb b/config/routes.rb
index 30681356c5f..a2acf170a6b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,8 @@ Rails.application.routes.draw do
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
+ # Emojis
+ resources :emojis, only: :index
# Search
get 'search' => 'search#show'
@@ -154,6 +156,11 @@ Rails.application.routes.draw do
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+ # Appearance
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
@@ -251,6 +258,14 @@ Rails.application.routes.draw do
end
end
+ resource :appearances, path: 'appearance' do
+ member do
+ get :preview
+ delete :logo
+ delete :header_logos
+ end
+ end
+
resource :application_settings, only: [:show, :update] do
resources :services
put :reset_runners_token
diff --git a/db/migrate/20160222153918_create_appearances_ce.rb b/db/migrate/20160222153918_create_appearances_ce.rb
new file mode 100644
index 00000000000..5e66d5094bd
--- /dev/null
+++ b/db/migrate/20160222153918_create_appearances_ce.rb
@@ -0,0 +1,14 @@
+class CreateAppearancesCE < ActiveRecord::Migration
+ def change
+ unless table_exists?(:appearances)
+ create_table :appearances do |t|
+ t.string :title
+ t.text :description
+ t.string :header_logo
+ t.string :logo
+
+ t.timestamps null: false
+ end
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4708c29d9ae..53a941d30de 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: 20160220123949) do
+ActiveRecord::Schema.define(version: 20160222153918) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -24,6 +24,15 @@ ActiveRecord::Schema.define(version: 20160220123949) do
t.datetime "updated_at"
end
+ create_table "appearances", force: :cascade do |t|
+ t.string "title"
+ t.text "description"
+ t.string "header_logo"
+ t.string "logo"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "application_settings", force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
diff --git a/doc/README.md b/doc/README.md
index 5089e1e70f6..be6c5f96ea1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -16,40 +16,42 @@
- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-## CI Documentation
+## CI User documentation
-- [Quick Start](ci/quick_start/README.md)
-- [Enable or disable GitLab CI](ci/enable_or_disable_ci.md)
-- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
-- [Configuring runner](ci/runners/README.md)
-- [Configuring deployment](ci/deployment/README.md)
-- [Using Docker Images](ci/docker/using_docker_images.md)
-- [Using Docker Build](ci/docker/using_docker_build.md)
-- [Using Variables](ci/variables/README.md)
-- [Using SSH keys](ci/ssh_keys/README.md)
+- [Get started with GitLab CI](ci/quick_start/README.md)
+- [Learn how to enable or disable GitLab CI](ci/enable_or_disable_ci.md)
+- [Learn how `.gitlab-ci.yml` works](ci/yaml/README.md)
+- [Configure a Runner, the application that runs your builds](ci/runners/README.md)
+- [Use Docker images with GitLab Runner](ci/docker/using_docker_images.md)
+- [Use CI to build Docker images](ci/docker/using_docker_build.md)
+- [Use variables in your `.gitlab-ci.yml`](ci/variables/README.md)
+- [Use SSH keys in your build environment](ci/ssh_keys/README.md)
+- [Trigger builds through the API](ci/triggers/README.md)
+- [Build artifacts](ci/build_artifacts/README.md)
- [User permissions](ci/permissions/README.md)
- [API](ci/api/README.md)
-- [Triggering builds through the API](ci/triggers/README.md)
-- [Build artifacts](ci/build_artifacts/README.md)
-### CI Languages
+### CI Examples
-- [Testing PHP](ci/languages/php.md)
+- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+- [Test your PHP applications](ci/examples/php.md)
+- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
+- [Test Clojure applications](ci/examples/test-clojure-application.md)
+- [Using `dpl` as deployment tool](ci/deployment/README.md)
+- Help your favorite programming language and GitLab by sending a merge request
+ with a guide for that language.
### CI Services
+GitLab CI uses the `services` keyword to define what docker containers should
+be linked with your base image. Below is a list of examples you may use:
+
- [Using MySQL](ci/services/mysql.md)
- [Using PostgreSQL](ci/services/postgres.md)
- [Using Redis](ci/services/redis.md)
- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
-### CI Examples
-
-- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-- [Test Clojure applications](ci/examples/test-clojure-application.md)
-- Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
-
## Administrator documentation
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 5886829be51..2120b5b2850 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,39 +1,37 @@
## GitLab CI Documentation
-### User documentation
-
-* [Quick Start](quick_start/README.md)
-* [Enable or disable GitLab CI](enable_or_disable_ci.md)
-* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
-* [Configuring runner](runners/README.md)
-* [Configuring deployment](deployment/README.md)
-* [Using Docker Images](docker/using_docker_images.md)
-* [Using Docker Build](docker/using_docker_build.md)
-* [Using Variables](variables/README.md)
-* [Using SSH keys](ssh_keys/README.md)
-* [Triggering builds through the API](triggers/README.md)
-* [Build artifacts](build_artifacts/README.md)
-
-### Languages
-
-* [Testing PHP](languages/php.md)
-
-### Services
-
-* [Using MySQL](services/mysql.md)
-* [Using PostgreSQL](services/postgres.md)
-* [Using Redis](services/redis.md)
-* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
-
-### Examples
-
-+ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-+ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](examples/test-clojure-application.md)
-+ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
-
-### Administrator documentation
-
-* [User permissions](permissions/README.md)
-* [API](api/README.md)
+### CI User documentation
+
+- [Get started with GitLab CI](quick_start/README.md)
+- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
+- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
+- [Configure a Runner, the application that runs your builds](runners/README.md)
+- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
+- [Use CI to build Docker images](docker/using_docker_build.md)
+- [Use variables in your `.gitlab-ci.yml`](variables/README.md)
+- [Use SSH keys in your build environment](ssh_keys/README.md)
+- [Trigger builds through the API](triggers/README.md)
+- [Build artifacts](build_artifacts/README.md)
+- [User permissions](permissions/README.md)
+- [API](api/README.md)
+
+### CI Examples
+
+- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+- [Test your PHP applications](examples/php.md)
+- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
+- [Test Clojure applications](examples/test-clojure-application.md)
+- [Using `dpl` as deployment tool](deployment/README.md)
+- Help your favorite programming language and GitLab by sending a merge request
+ with a guide for that language.
+
+### CI Services
+
+GitLab CI uses the `services` keyword to define what docker containers should
+be linked with your base image. Below is a list of examples you may use:
+
+- [Using MySQL](services/mysql.md)
+- [Using PostgreSQL](services/postgres.md)
+- [Using Redis](services/redis.md)
+- [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 1cf41aea391..31f29f4a082 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,5 +1,13 @@
-# Build script examples
+## Build script examples
-+ [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-+ [Test a Clojure application](test-clojure-application.md)
+- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
+- [Test a Clojure application](test-clojure-application.md)
+
+## Languages
+
+This is a list of languages you can test with GitLab CI. Each section has
+comprehensive documentation and comes with a test repository hosted on
+GitLab.com.
+
+- [Testing PHP](php.md)
diff --git a/doc/ci/languages/php.md b/doc/ci/examples/php.md
index aeadd6a448e..aeadd6a448e 100644
--- a/doc/ci/languages/php.md
+++ b/doc/ci/examples/php.md
diff --git a/doc/ci/languages/README.md b/doc/ci/languages/README.md
deleted file mode 100644
index 54b2343e08b..00000000000
--- a/doc/ci/languages/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-### Languages
-
-This is a list of languages you can test with GitLab CI. Each section has
-comprehensive documentation and comes with a test repository hosted on
-GitLab.com
-
-+ [Testing PHP](php.md)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 07e566e3710..5af7be5581e 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -124,7 +124,7 @@ In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`.
A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker
container or even a cluster of containers. GitLab and the Runners communicate
through an API, so the only needed requirement is that the machine on which the
-Runner is configured to has Internet access.
+Runner is configured to have Internet access.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -201,6 +201,11 @@ You can access a builds badge image using following link:
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
+## Examples
+
+Visit the [examples README][examples] to see a list of examples using GitLab
+CI with various languages.
+
## Next steps
Awesome! You started using CI in GitLab!
@@ -212,3 +217,4 @@ Visit our various languages examples at <https://gitlab.com/groups/gitlab-exampl
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
+[examples]: ../examples/README.md
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 1ebb0a4a250..4b79461d55c 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,9 +1,9 @@
## GitLab CI Services
-GitLab CI uses the `services` keyword to define what docker containers should be
-linked with your base image. Below is a list of examples you may use.
+GitLab CI uses the `services` keyword to define what docker containers should
+be linked with your base image. Below is a list of examples you may use.
-+ [Using MySQL](mysql.md)
-+ [Using PostgreSQL](postgres.md)
-+ [Using Redis](redis.md)
-+ [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
+- [Using MySQL](mysql.md)
+- [Using PostgreSQL](postgres.md)
+- [Using Redis](redis.md)
+- [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 0edb56dc20e..051eaa04152 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -518,3 +518,10 @@ You can find the link under `/ci/lint` of your gitlab instance.
If your commit message contains `[ci skip]`, the commit will be created but the
builds will be skipped.
+
+## Examples
+
+Visit the [examples README][examples] to see a list of examples using GitLab
+CI with various languages.
+
+[examples]: ../examples/README.md
diff --git a/doc/customization/branded_login_page.md b/doc/customization/branded_login_page.md
new file mode 100644
index 00000000000..d4d9f5f7b5e
--- /dev/null
+++ b/doc/customization/branded_login_page.md
@@ -0,0 +1,19 @@
+# Changing the appearance of the login page
+
+GitLab Community Edition offers a way to put your company's identity on the login page of your GitLab server and make it a branded login page.
+
+By default, the page shows the GitLab logo and description.
+
+![default_login_page](branded_login_page/default_login_page.png)
+
+## Changing the appearance of the login page
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Fill in the required details like Title, Description and upload the company logo.
+
+![appearance](branded_login_page/appearance.png)
+
+After saving the page, your GitLab login page will have the details you filled in:
+
+![company_login_page](branded_login_page/custom_sign_in.png)
diff --git a/doc/customization/branded_login_page/appearance.png b/doc/customization/branded_login_page/appearance.png
new file mode 100644
index 00000000000..6bce1f0a287
--- /dev/null
+++ b/doc/customization/branded_login_page/appearance.png
Binary files differ
diff --git a/doc/customization/branded_login_page/custom_sign_in.png b/doc/customization/branded_login_page/custom_sign_in.png
new file mode 100644
index 00000000000..d6020b029a2
--- /dev/null
+++ b/doc/customization/branded_login_page/custom_sign_in.png
Binary files differ
diff --git a/doc/customization/branded_login_page/default_login_page.png b/doc/customization/branded_login_page/default_login_page.png
new file mode 100644
index 00000000000..795c7954d8e
--- /dev/null
+++ b/doc/customization/branded_login_page/default_login_page.png
Binary files differ
diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md
index e993230bb88..a0cb234bea0 100644
--- a/doc/customization/welcome_message.md
+++ b/doc/customization/welcome_message.md
@@ -1,12 +1,12 @@
-# Customize the complete sign-in page (GitLab Enterprise Edition only)
+# Customize the complete sign-in page
-Please see [Branded login page](http://doc.gitlab.com/ee/customization/branded_login_page.html)
+Please see [Branded login page](branded_login_page.md)
# Add a welcome message to the sign-in page (GitLab Community Edition)
It is possible to add a markdown-formatted welcome message to your GitLab
sign-in page. Users of GitLab Enterprise Edition should use the [branded login
-page feature](/ee/customization/branded_login_page.html) instead.
+page feature](branded_login_page.md) instead.
The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI.
-Admin area > Settings \ No newline at end of file
+Admin area > Settings
diff --git a/doc/development/README.md b/doc/development/README.md
index d5bf166ad32..b9a0d81e5ba 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,11 +1,12 @@
# Development
- [Architecture](architecture.md) of GitLab
-- [Shell commands](shell_commands.md) in the GitLab codebase
-- [Rake tasks](rake_tasks.md) for development
+- [Benchmarking](benchmarking.md)
- [CI setup](ci_setup.md) for testing GitLab
+- [Gotchas](gotchas.md) to avoid
+- [How to dump production data to staging](db_dump.md)
+- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
+- [Rake tasks](rake_tasks.md) for development
+- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
-- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
-- [How to dump production data to staging](dump_db.md)
-- [Benchmarking](benchmarking.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 6101a71a8de..12e33406cb6 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -42,7 +42,7 @@ Gitlab-shell communicates with Sidekiq via the “communication board” (Redis)
## System Layout
-When referring to ~git in the pictures it means the home directory of the git user which is typically /home/git.
+When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git.
GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable).
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
new file mode 100644
index 00000000000..21078c8d6f9
--- /dev/null
+++ b/doc/development/gotchas.md
@@ -0,0 +1,103 @@
+# Gotchas
+
+The purpose of this guide is to document potential "gotchas" that contributors
+might encounter or should avoid during development of GitLab CE and EE.
+
+## Don't `describe` symbols
+
+Consider the following model spec:
+
+```ruby
+require 'rails_helper'
+
+describe User do
+ describe :to_param do
+ it 'converts the username to a param' do
+ user = described_class.new(username: 'John Smith')
+
+ expect(user.to_param).to eq 'john-smith'
+ end
+ end
+end
+```
+
+When run, this spec doesn't do what we might expect:
+
+```sh
+spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMethodError: undefined method `new' for :to_param:Symbol
+```
+
+### Solution
+
+Except for the top-level `describe` block, always provide a String argument to
+`describe`.
+
+## Don't `rescue Exception`
+
+See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
+
+_**Note:** This rule is [enforced automatically by
+Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L911-914)._
+
+[Exception]: http://stackoverflow.com/q/10048173/223897
+
+## Don't use inline CoffeeScript in views
+
+Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
+performance overhead.
+
+_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-5-stable/config/initializers/haml.rb)
+in an initializer._
+
+### Further reading
+
+- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
+- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
+
+## ID-based CSS selectors need to be a bit more specific
+
+Normally, because HTML `id` attributes need to be unique to the page, it's
+perfectly fine to write some JavaScript like the following:
+
+```javascript
+$('#js-my-selector').hide();
+```
+
+However, there's a feature of GitLab's Markdown processing that [automatically
+adds anchors to header elements][ToC Processing], with the `id` attribute being
+automatically generated based on the content of the header.
+
+Unfortunately, this feature makes it possible for user-generated content to
+create a header element with the same `id` attribute we're using in our
+selector, potentially breaking the JavaScript behavior. A user could break the
+above example with the following Markdown:
+
+```markdown
+## JS My Selector
+```
+
+Which gets converted to the following HTML:
+
+```html
+<h2>
+ <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a>
+ JS My Selector
+</h2>
+```
+
+[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37
+
+### Solution
+
+The current recommended fix for this is to make our selectors slightly more
+specific:
+
+```javascript
+$('div#js-my-selector').hide();
+```
+
+### Further reading
+
+- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908)
+- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023)
+- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024)
diff --git a/doc/gitlab-basics/basicsimages/compare_braches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
index 7eebaed9075..7eebaed9075 100644
--- a/doc/gitlab-basics/basicsimages/compare_braches.png
+++ b/doc/gitlab-basics/basicsimages/compare_branches.png
Binary files differ
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2a5b99609e4..c1787a7c6a8 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -265,8 +265,9 @@ sudo usermod -aG redis git
# Create the public/uploads/ directory
sudo -u git -H mkdir public/uploads/
- # Make sure GitLab can write to the public/uploads/ directory
- sudo chmod -R u+rwX public/uploads
+ # Make sure only the GitLab user has access to the public/uploads/ directory
+ # now that files in public/uploads are served by gitlab-workhorse
+ sudo chmod 0700 public/uploads
# Change the permissions of the directory where CI build traces are stored
sudo chmod -R u+rwX builds/
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3cab677fdc7..8df142c531b 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -67,8 +67,8 @@ You need at least 2GB of addressable memory (RAM + swap) to install and use GitL
With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage.
- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
-- 1GB RAM + 1GB swap supports up to 100 users but it will be slow
-- **2GB RAM** is the **recommended** memory size and supports up to 100 users
+- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow
+- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
- 4GB RAM supports up to 1,000 users
- 8GB RAM supports up to 2,000 users
- 16GB RAM supports up to 4,000 users
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index f97c252fd7c..59803aea080 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -18,7 +18,7 @@ You accept and agree to the following terms and conditions for Your present and
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]".
+7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 168e7d143ee..ac0fd3d1756 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -6,7 +6,7 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
-On public projects the Guest role is not enforced.
+On public and internal projects the Guest role is not enforced.
All users will be able to create issues, leave comments, and pull or download the project code.
To add or import a user, you can follow the [project users and members
@@ -26,6 +26,7 @@ documentation](../workflow/add-user/add-user.md).
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
+| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| Manage merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -35,6 +36,7 @@ documentation](../workflow/add-user/add-user.md).
| Add tags | | | ✓ | ✓ | ✓ |
| Write a wiki | | | ✓ | ✓ | ✓ |
| Cancel and retry builds | | | ✓ | ✓ | ✓ |
+| Create or update commit status | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 0b205ea6de7..1b354bcc0f1 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -16,7 +16,7 @@ It offers a simple, transparent and effective way to work with git.
![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png)
When converting to git you have to get used to the fact that there are three steps before a commit is shared with colleagues.
-Most version control systems have only step, committing from the working copy to a shared server.
+Most version control systems have only one step, committing from the working copy to a shared server.
In git you add files from the working copy to the staging area. After that you commit them to the local repo.
The third step is pushing to a shared remote repository.
After getting used to these three steps the branching model becomes the challenge.
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index b355a91b5a6..4828bb5dce6 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -69,6 +69,7 @@ branches and tags.
```bash
git remote add origin git@gitlab.com:<group>/<project>.git
git push --all origin
+git push --tags origin
```
## Contribute to this guide
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
index e5914aa8e67..83e562d6929 100644
--- a/doc/workflow/shortcuts.png
+++ b/doc/workflow/shortcuts.png
Binary files differ
diff --git a/features/admin/appearance.feature b/features/admin/appearance.feature
new file mode 100644
index 00000000000..5c1dd7531c1
--- /dev/null
+++ b/features/admin/appearance.feature
@@ -0,0 +1,37 @@
+Feature: Admin Appearance
+ Scenario: Create new appearance
+ Given I sign in as an admin
+ And I visit admin appearance page
+ When submit form with new appearance
+ Then I should be redirected to admin appearance page
+ And I should see newly created appearance
+
+ Scenario: Preview appearance
+ Given application has custom appearance
+ And I sign in as an admin
+ When I visit admin appearance page
+ And I click preview button
+ Then I should see a customized appearance
+
+ Scenario: Custom sign-in page
+ Given application has custom appearance
+ When I visit login page
+ Then I should see a customized appearance
+
+ Scenario: Appearance logo
+ Given application has custom appearance
+ And I sign in as an admin
+ And I visit admin appearance page
+ When I attach a logo
+ Then I should see a logo
+ And I remove the logo
+ Then I should see logo removed
+
+ Scenario: Header logos
+ Given application has custom appearance
+ And I sign in as an admin
+ And I visit admin appearance page
+ When I attach header logos
+ Then I should see header logos
+ And I remove the header logos
+ Then I should see header logos removed
diff --git a/features/dashboard/event_filters.feature b/features/dashboard/event_filters.feature
index 96399ea21a6..8c3ff64164f 100644
--- a/features/dashboard/event_filters.feature
+++ b/features/dashboard/event_filters.feature
@@ -43,10 +43,16 @@ Feature: Event Filters
And I should not see new member event
When I click "team" event filter
And I visit dashboard activity page
- Then I should see push event
+ Then I should not see push event
And I should see new member event
And I should not see merge request event
When I click "push" event filter
- Then I should not see push event
- And I should see new member event
+ And I visit dashboard activity page
+ Then I should see push event
+ And I should not see new member event
And I should not see merge request event
+ When I click "merge" event filter
+ And I visit dashboard activity page
+ Then I should see merge request event
+ And I should not see push event
+ And I should not see new member event
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
index 629859e960d..7df6b6f09ba 100644
--- a/features/explore/projects.feature
+++ b/features/explore/projects.feature
@@ -87,6 +87,7 @@ Feature: Explore Projects
Scenario: I visit public project issues page as a non authorized user
Given I visit project "Community" page
+ Then I should not see command line instructions
And I visit "Community" issues page
Then I should see list of issues for "Community" project
diff --git a/features/login_form.feature b/features/login_form.feature
deleted file mode 100644
index b4d95754482..00000000000
--- a/features/login_form.feature
+++ /dev/null
@@ -1,5 +0,0 @@
-Feature: Login form
- Scenario: I see crowd form
- Given Crowd integration enabled
- When I visit sign in page
- Then I should see Crowd login form \ No newline at end of file
diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature
index 4f3fd194d00..3c029a973df 100644
--- a/features/project/builds/summary.feature
+++ b/features/project/builds/summary.feature
@@ -3,6 +3,7 @@ Feature: Project Builds Summary
Given I sign in as a user
And I own a project
And project has CI enabled
+ And project has coverage enabled
And project has a recent build
Scenario: I browse build details page
@@ -12,6 +13,7 @@ Feature: Project Builds Summary
Scenario: I browse project builds page
When I visit project builds page
+ Then I see coverage
Then I see button to CI Lint
Scenario: I erase a build
diff --git a/features/project/milestone.feature b/features/project/milestone.feature
index e0f4c0e9d7c..713f0f3b979 100644
--- a/features/project/milestone.feature
+++ b/features/project/milestone.feature
@@ -13,6 +13,7 @@ Feature: Project Milestone
Given I visit project "Shop" milestones page
And I click link "v2.2"
Then I should see the labels "bug", "enhancement" and "feature"
+ And I should see the "bug" label listed only once
@javascript
Scenario: Listing labels from labels tab
diff --git a/features/project/project.feature b/features/project/project.feature
index 1a53945eb04..f1f3ed26065 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -86,3 +86,9 @@ Feature: Project
Given I click notifications drop down button
When I choose Mention setting
Then I should see Notification saved message
+
+ Scenario: I should see command line instructions
+ Given I own an empty project
+ And I visit my empty project page
+ And I create bare repo
+ Then I should see command line instructions
diff --git a/features/steps/admin/appearance.rb b/features/steps/admin/appearance.rb
new file mode 100644
index 00000000000..0d1be46d11d
--- /dev/null
+++ b/features/steps/admin/appearance.rb
@@ -0,0 +1,72 @@
+class Spinach::Features::AdminAppearance < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+
+ step 'submit form with new appearance' do
+ fill_in 'appearance_title', with: 'MyCompany'
+ fill_in 'appearance_description', with: 'dev server'
+ click_button 'Save'
+ end
+
+ step 'I should be redirected to admin appearance page' do
+ expect(current_path).to eq admin_appearances_path
+ expect(page).to have_content 'Appearance settings'
+ end
+
+ step 'I should see newly created appearance' do
+ expect(page).to have_field('appearance_title', with: 'MyCompany')
+ expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_content 'Last edit'
+ end
+
+ step 'I click preview button' do
+ click_link "Preview"
+ end
+
+ step 'application has custom appearance' do
+ create(:appearance)
+ end
+
+ step 'I should see a customized appearance' do
+ expect(page).to have_content appearance.title
+ expect(page).to have_content appearance.description
+ end
+
+ step 'I attach a logo' do
+ attach_file(:appearance_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Save'
+ end
+
+ step 'I attach header logos' do
+ attach_file(:appearance_header_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Save'
+ end
+
+ step 'I should see a logo' do
+ expect(page).to have_xpath('//img[@src="/uploads/appearance/logo/1/dk.png"]')
+ end
+
+ step 'I should see header logos' do
+ expect(page).to have_xpath('//img[@src="/uploads/appearance/header_logo/1/dk.png"]')
+ end
+
+ step 'I remove the logo' do
+ click_link 'Remove logo'
+ end
+
+ step 'I remove the header logos' do
+ click_link 'Remove header logo'
+ end
+
+ step 'I should see logo removed' do
+ expect(page).not_to have_xpath('//img[@src="/uploads/appearance/logo/1/gitlab_logo.png"]')
+ end
+
+ step 'I should see header logos removed' do
+ expect(page).not_to have_xpath('//img[@src="/uploads/appearance/header_logo/1/header_logo_light.png"]')
+ end
+
+ def appearance
+ Appearance.last
+ end
+end
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 742ba5d71f6..cb6fa8a47da 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -18,7 +18,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
end
step 'I should see empty public project details' do
- expect(page).to have_content 'Git global setup'
+ expect(page).not_to have_content 'Git global setup'
end
step 'I should see empty public project details with http clone info' do
diff --git a/features/steps/login_form.rb b/features/steps/login_form.rb
deleted file mode 100644
index b9ff6ae67fd..00000000000
--- a/features/steps/login_form.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::LoginForm < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
- include SharedUser
- include SharedSearch
-
- step 'Crowd integration enabled' do
- @providers_orig = Gitlab::OAuth::Provider.providers
- @omniauth_conf_orig = Gitlab.config.omniauth.enabled
- expect(Gitlab::OAuth::Provider).to receive(:providers).and_return([:crowd])
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
- expect(Gitlab.config.omniauth).to receive(:enabled).and_return(true)
- end
-
- step 'I should see Crowd login form' do
- expect(page).to have_selector '#tab-crowd form'
- Gitlab::OAuth::Provider.stub(:providers).and_return(@providers_orig)
- Gitlab.config.omniauth.stub(:enabled).and_return(@omniauth_conf_orig)
- end
-
- step 'I visit sign in page' do
- visit new_user_session_path
- end
-end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 0305f7e6da0..7895f643d0c 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -27,9 +27,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I change my avatar' do
- attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save changes"
- @user.reload
+ attach_avatar
end
step 'I should see new avatar' do
@@ -42,9 +40,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I have an avatar' do
- attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save changes"
- @user.reload
+ attach_avatar
end
step 'I remove my avatar' do
@@ -97,7 +93,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step "I should see a password error message" do
- page.within '.alert' do
+ page.within '.alert-danger' do
expect(page).to have_content "Password confirmation doesn't match"
end
end
@@ -233,4 +229,16 @@ 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 "Save changes"
+
+ @user.reload
+ end
end
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
index 4688a0e2096..e9e2359146e 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -4,6 +4,12 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
include SharedBuilds
include RepoHelpers
+ step 'I see coverage' do
+ page.within('td.coverage') do
+ expect(page).to have_content "99.9%"
+ end
+ end
+
step 'I see button to CI Lint' do
page.within('.nav-controls') do
ci_lint_tool_link = page.find_link('CI Lint')
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 93cf608cc62..277c63914d1 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -96,6 +96,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'The search field is focused' do
- page.evaluate_script("document.activeElement.id").should eq "emoji_search"
+ expect(page).to have_selector('#emoji_search')
+ expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
end
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 37bf52b4a95..ef185861e00 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -144,4 +144,14 @@ class Spinach::Features::Project < Spinach::FeatureSteps
expect(page).to have_content 'Notification settings saved'
end
end
+
+ step 'I create bare repo' do
+ click_link 'Create empty bare repository'
+ end
+
+ step 'I should see command line instructions' do
+ page.within ".empty_wrapper" do
+ expect(page).to have_content("Command line instructions")
+ end
+ end
end
diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb
index ec881c0d8fc..2508c09e36d 100644
--- a/features/steps/project/project_milestone.rb
+++ b/features/steps/project/project_milestone.rb
@@ -41,6 +41,12 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end
end
+ step 'I should see the "bug" label listed only once' do
+ page.within('#tab-labels') do
+ expect(page).to have_content('bug', count: 1)
+ end
+ end
+
step 'I click link "v2.2"' do
click_link "v2.2"
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 3a4f7a6e01c..2134dae168a 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -238,7 +238,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see new wiki page named test' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test")
- expect(page).to have_content "Edit Page test"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Test"
+ expect(page).to have_content "Edit Page"
+ end
end
When 'I go back to wiki page home' do
@@ -252,7 +256,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Gitlab API document' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
- expect(page).to have_content "Edit Page api"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Edit"
+ expect(page).to have_content "Api"
+ end
end
step 'I click on Rake tasks link' do
@@ -261,7 +269,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Rake tasks directory' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
- expect(page).to have_content "Edit Page raketasks"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Edit"
+ expect(page).to have_content "Rake"
+ end
end
step 'I go directory which contains README file' do
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 2a735afbe7b..223b7277b51 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -120,7 +120,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Edit Page image.jpg')
+ expect(page).to have_content('Edit Page')
end
step 'I create a New page with paths' do
@@ -159,7 +159,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I should see the page history' do
- expect(page).to have_content('History for')
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content('History')
+ end
end
step 'I search for Wiki content' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 0bd5d93b997..f33ed7834fe 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -5,9 +5,13 @@ module SharedBuilds
@project.enable_ci
end
+ step 'project has coverage enabled' do
+ @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
+ end
+
step 'project has a recent build' do
@ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha)
- @build = create(:ci_build, commit: @ci_commit)
+ @build = create(:ci_build_with_coverage, commit: @ci_commit)
end
step 'recent build is successful' do
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index f4df4874d2f..6432a786bfc 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -7,6 +7,10 @@ module SharedPaths
visit new_project_path
end
+ step 'I visit login page' do
+ visit new_user_session_path
+ end
+
# ----------------------------------------
# User
# ----------------------------------------
@@ -187,6 +191,10 @@ module SharedPaths
visit admin_groups_path
end
+ step 'I visit admin appearance page' do
+ visit admin_appearances_path
+ end
+
step 'I visit admin teams page' do
visit admin_teams_path
end
@@ -447,6 +455,10 @@ module SharedPaths
visit namespace_project_path(project.namespace, project)
end
+ step "I should not see command line instructions" do
+ expect(page).not_to have_css('.empty_wrapper')
+ end
+
# ----------------------------------------
# Public Projects
# ----------------------------------------
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 38069ff8835..f33379f76c9 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -6,7 +6,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
Capybara.default_wait_time = timeout
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 04ddfe53ed6..abd79b329ae 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,6 +7,8 @@ module Banzai
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
+ UNSAFE_PROTOCOLS = %w(javascript :javascript data vbscript).freeze
+
def whitelist
whitelist = super
@@ -43,8 +45,8 @@ module Banzai
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
- # ...but then remove links with the `javascript` protocol
- whitelist[:transformers].push(remove_javascript_links)
+ # ...but then remove links with unsafe protocols
+ whitelist[:transformers].push(remove_unsafe_links)
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(remove_rel)
@@ -55,14 +57,14 @@ module Banzai
whitelist
end
- def remove_javascript_links
+ def remove_unsafe_links
lambda do |env|
node = env[:node]
return unless node.name == 'a'
return unless node.has_attribute?('href')
- if node['href'].start_with?('javascript', ':javascript')
+ if node['href'].start_with?(*UNSAFE_PROTOCOLS)
node.remove_attribute('href')
end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 81099cb8ba9..d59872dc3a2 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -266,7 +266,7 @@ namespace :gitlab do
unless File.directory?(Rails.root.join('public/uploads'))
puts "no".red
try_fixing_it(
- "sudo -u #{gitlab_user} mkdir -m 750 #{Rails.root}/public/uploads"
+ "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -278,21 +278,22 @@ namespace :gitlab do
upload_path = File.realpath(Rails.root.join('public/uploads'))
upload_path_tmp = File.join(upload_path, 'tmp')
- if File.stat(upload_path).mode == 040750
+ if File.stat(upload_path).mode == 040700
unless Dir.exists?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.magenta
return
end
- # if tmp upload dir has incorrect permissions, assume others do as well
- if File.stat(upload_path_tmp).mode == 040755 && File.owned?(upload_path_tmp) # verify drwxr-xr-x permissions
+ # If tmp upload dir has incorrect permissions, assume others do as well
+ # Verify drwx------ permissions
+ if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
puts "yes".green
else
puts "no".red
try_fixing_it(
"sudo chown -R #{gitlab_user} #{upload_path}",
"sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
- "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0755 {} \\;"
+ "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -302,7 +303,7 @@ namespace :gitlab do
else
puts "no".red
try_fixing_it(
- "sudo chmod 0750 #{upload_path}",
+ "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
)
for_more_information(
see_installation_guide_section "GitLab"
diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh
new file mode 100755
index 00000000000..0a4239e132c
--- /dev/null
+++ b/scripts/notify_slack.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Sends Slack notification ERROR_MSG to CHANNEL
+# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
+
+CHANNEL=$1
+ERROR_MSG=$2
+
+if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then
+ echo "Missing argument(s) - Use: $0 channel message"
+ echo "and set CI_SLACK_WEBHOOK_URL environment variable."
+else
+ curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL"
+fi \ No newline at end of file
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 85379a8e984..410b993fdfb 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -21,7 +21,7 @@ describe AutocompleteController do
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 1 }
- it { expect(body.first["username"]).to eq user.username }
+ it { expect(body.map { |u| u["username"] }).to include(user.username) }
end
describe 'GET #users with unknown project' do
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
index 77436958711..d4a380cc2ee 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, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:user) { create(:user, :with_avatar) }
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 ad5855df0a4..85dff009bcf 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, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) }
+ let(:user) { create(:user, :with_avatar) }
before do
sign_in(user)
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index af5d043cf02..0d9f4b299bc 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, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:user) { create(:user, :with_avatar) }
describe "GET show" do
context "when viewing a user avatar" do
diff --git a/spec/factories.rb b/spec/factories.rb
index 2a81684dfcf..264e3ed2c8d 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -36,6 +36,13 @@ 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
ignore do
extern_uid '123456'
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
new file mode 100644
index 00000000000..cf2a2b76bcb
--- /dev/null
+++ b/spec/factories/appearances.rb
@@ -0,0 +1,8 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :appearance do
+ title "MepMep"
+ description "This is my Community Edition instance"
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index a46466798d6..a7a54d44521 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -53,6 +53,10 @@ FactoryGirl.define do
tag true
end
+ factory :ci_build_with_coverage do
+ coverage 99.9
+ end
+
trait :trace do
after(:create) do |build, evaluator|
build.trace = 'BUILD TRACE'
@@ -68,7 +72,7 @@ FactoryGirl.define do
build.artifacts_metadata =
fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
'application/x-gzip')
-
+
build.save!
end
end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 38c8d343ce3..591866b40d4 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
- expect(page).to have_css('.title', count: 1)
+ expect(page).to have_css('.issue .title', count: 1)
end
scenario 'filters by a specific Milestone', js: true do
@@ -23,7 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
- expect(page).to have_css('.title', count: 1)
+ expect(page).to have_css('.issue .title', count: 1)
end
def visit_issues(project)
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f6c1005d265..8013b31524f 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, avatar: File.open(avatar_file_path))
+ user = create(:user, :with_avatar, 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, avatar: File.open(avatar_file_path))
+ user = create(:user, :with_avatar, 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, avatar: File.open(avatar_file_path))
+ user = create(:user, :with_avatar, 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 e14a6dbf922..4a7b00c7660 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -156,13 +156,27 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
}
protocols.each do |name, data|
- it "handles #{name}" do
+ it "disallows #{name}" do
doc = filter(data[:input])
expect(doc.to_html).to eq data[:output]
end
end
+ it 'disallows data links' do
+ input = '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">XSS</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>XSS</a>'
+ end
+
+ it 'disallows vbscript links' do
+ input = '<a href="vbscript:alert(document.domain)">XSS</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>XSS</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/status_spec.rb b/spec/lib/ci/status_spec.rb
index a2eb14f3a97..1539720bb8d 100644
--- a/spec/lib/ci/status_spec.rb
+++ b/spec/lib/ci/status_spec.rb
@@ -4,38 +4,68 @@ describe Ci::Status do
describe '.get_status' do
subject { described_class.get_status(statuses) }
- [:ci_build, :generic_commit_status].each do |type|
- context "for #{type}" do
- context 'all successful' do
- let(:statuses) { Array.new(2) { create(type, status: :success) } }
- it { is_expected.to eq 'success' }
- end
+ shared_examples 'build status summary' do
+ context 'all successful' do
+ let(:statuses) { Array.new(2) { create(type, status: :success) } }
+ it { is_expected.to eq 'success' }
+ end
- context 'at least one failed' do
- let(:statuses) { [create(type, status: :success), create(type, status: :failed)] }
- it { is_expected.to eq 'failed' }
+ context 'at least one failed' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :failed)]
end
- context 'at least one running' do
- let(:statuses) { [create(type, status: :success), create(type, status: :running)] }
- it { is_expected.to eq 'running' }
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'at least one running' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :running)]
end
- context 'at least one pending' do
- let(:statuses) { [create(type, status: :success), create(type, status: :pending)] }
- it { is_expected.to eq 'running' }
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'at least one pending' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :pending)]
end
- context 'success and failed but allowed to fail' do
- let(:statuses) { [create(type, status: :success), create(type, status: :failed, allow_failure: true)] }
- it { is_expected.to eq 'success' }
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'success and failed but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :failed, allow_failure: true)]
end
- context 'one failed but allowed to fail' do
- let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
- it { is_expected.to eq 'success' }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one failed but allowed to fail' do
+ let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one finished and second running but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :running, allow_failure: true)]
end
+
+ it { is_expected.to eq 'running' }
end
end
+
+ context 'ci build statuses' do
+ let(:type) { :ci_build }
+ it_behaves_like 'build status summary'
+ end
+
+ context 'generic commit statuses' do
+ let(:type) { :generic_commit_status }
+ it_behaves_like 'build status summary'
+ end
end
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
new file mode 100644
index 00000000000..c5658bd26e1
--- /dev/null
+++ b/spec/models/appearance_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Appearance, type: :model do
+ subject { create(:appearance) }
+
+ it { is_expected.to be_valid }
+
+ it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_presence_of(:description) }
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ecf37b40c58..253902512c3 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -118,4 +118,38 @@ eos
it { expect(data[:modified]).to eq([".gitmodules"]) }
it { expect(data[:removed]).to eq([]) }
end
+
+ describe '#reverts_commit?' do
+ let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+
+ context 'commit has no description' do
+ before { allow(commit).to receive(:description?).and_return(false) }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ end
+
+ context "another_commit's description does not revert commit" do
+ before { allow(commit).to receive(:description).and_return("Foo Bar") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ end
+
+ context "another_commit's description reverts commit" do
+ before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ end
+
+ context "another_commit's description reverts merged merge request" do
+ before do
+ revert_description = "This reverts merge request !foo123"
+ allow(another_commit).to receive(:revert_description).and_return(revert_description)
+ allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
+ end
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 645ee0b929a..983848392b7 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -19,6 +19,10 @@
require 'spec_helper'
describe ProjectHook, models: true do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 7070aa4ac62..6ea99952a8f 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -18,20 +18,14 @@
require 'spec_helper'
-describe ProjectHook, models: true do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Mass assignment" do
- end
-
+describe WebHook, models: true do
describe "Validations" do
it { is_expected.to validate_presence_of(:url) }
- context "url format" do
+ describe 'url' do
it { is_expected.to allow_value("http://example.com").for(:url) }
- it { is_expected.to allow_value("https://excample.com").for(:url) }
+ it { is_expected.to allow_value("https://example.com").for(:url) }
+ it { is_expected.to allow_value(" https://example.com ").for(:url) }
it { is_expected.to allow_value("http://test.com/api").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
@@ -39,6 +33,12 @@ describe ProjectHook, models: true do
it { is_expected.not_to allow_value("example.com").for(:url) }
it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+
+ it 'strips :url before saving it' do
+ hook = create(:project_hook, url: ' https://example.com ')
+
+ expect(hook.url).to eq('https://example.com')
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index b596782f4e1..51ae2c04ed0 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -362,14 +362,14 @@ describe Repository, models: true do
repository.expire_cache('master')
end
- it 'expires the emptiness cache for an empty repository' do
+ it 'expires the emptiness caches for an empty repository' do
expect(repository).to receive(:empty?).and_return(true)
expect(repository).to receive(:expire_emptiness_caches)
repository.expire_cache
end
- it 'does not expire the emptiness cache for a non-empty repository' do
+ it 'does not expire the emptiness caches for a non-empty repository' do
expect(repository).to receive(:empty?).and_return(false)
expect(repository).to_not receive(:expire_emptiness_caches)
@@ -464,4 +464,108 @@ describe Repository, models: true do
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
end
end
+
+ describe '#before_delete' do
+ describe 'when a repository does not exist' do
+ before do
+ allow(repository).to receive(:exists?).and_return(false)
+ end
+
+ it 'does not flush caches that depend on repository data' do
+ expect(repository).to_not receive(:expire_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the emptiness caches' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.before_delete
+ end
+ end
+
+ describe 'when a repository exists' do
+ before do
+ allow(repository).to receive(:exists?).and_return(true)
+ end
+
+ it 'flushes the caches that depend on repository data' do
+ expect(repository).to receive(:expire_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the emptiness caches' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.before_delete
+ end
+ end
+ end
+
+ describe '#before_change_head' do
+ it 'flushes the branch cache' do
+ expect(repository).to receive(:expire_branch_cache)
+
+ repository.before_change_head
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_change_head
+ end
+ end
+
+ describe '#before_create_tag' do
+ it 'flushes the cache' do
+ expect(repository).to receive(:expire_cache)
+
+ repository.before_create_tag
+ end
+ end
+
+ describe '#after_import' do
+ it 'flushes the emptiness cachess' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.after_import
+ end
+ end
+
+ describe '#after_push_commit' do
+ it 'flushes the cache' do
+ expect(repository).to receive(:expire_cache).with('master')
+
+ repository.after_push_commit('master')
+ end
+ end
+
+ describe '#after_create_branch' do
+ it 'flushes the visible content cache' do
+ expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.after_create_branch
+ end
+ end
+
+ describe '#after_remove_branch' do
+ it 'flushes the visible content cache' do
+ expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.after_remove_branch
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb.rb b/spec/models/todo_spec.rb
index ac481bf9fbd..fe9ea7e7d1e 100644
--- a/spec/models/todo_spec.rb.rb
+++ b/spec/models/todo_spec.rb
@@ -37,20 +37,6 @@ describe Todo, models: true do
it { is_expected.to validate_presence_of(:user) }
end
- describe '#action_name' do
- it 'returns proper message when action is an assigment' do
- subject.action = Todo::ASSIGNED
-
- expect(subject.action_name).to eq 'assigned'
- end
-
- it 'returns proper message when action is a mention' do
- subject.action = Todo::MENTIONED
-
- expect(subject.action_name).to eq 'mentioned you on'
- end
- end
-
describe '#body' do
before do
subject.target = build(:issue, title: 'Bugfix')
@@ -69,21 +55,15 @@ describe Todo, models: true do
end
end
- describe '#target_iid' do
- let(:issue) { build(:issue, id: 1, iid: 5) }
-
- before do
- subject.target = issue
- end
-
- it 'returns target.iid when target respond to iid' do
- expect(subject.target_iid).to eq 5
+ describe '#done!' do
+ it 'changes state to done' do
+ todo = create(:todo, state: :pending)
+ expect { todo.done! }.to change(todo, :state).from('pending').to('done')
end
- it 'returns target_id when target does not respond to iid' do
- allow(issue).to receive(:respond_to?).with(:iid).and_return(false)
-
- expect(subject.target_iid).to eq 1
+ it 'does not raise error when is already done' do
+ todo = create(:todo, state: :done)
+ expect { todo.done! }.not_to raise_error
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 32d4f39b04a..88821dd0dad 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -174,6 +174,18 @@ describe User, models: true do
end
end
end
+
+ describe 'avatar' do
+ it 'only validates when avatar is present' 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
+ end
+ end
end
describe "Respond to" do
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index f285517cdac..52a302e0e1a 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -66,9 +66,25 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
end
+ context 'triggered by an old build' do
+ let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+ let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+
+ it "merges all merge requests with merge when build succeeds enabled" do
+ allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
+ allow(ci_commit).to receive(:success?).and_return(true)
+ allow(old_build).to receive(:sha).and_return('1234abcdef')
+
+ expect(MergeWorker).to_not receive(:perform_async)
+ service.trigger(old_build)
+ end
+ end
+
context 'commit status without ref' do
let(:commit_status) { create(:generic_commit_status, status: 'success') }
+ before { mr_merge_if_green_enabled }
+
it "doesn't merge a requests for status on other branch" do
allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index df3aa955f24..96420acb31d 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -110,6 +110,8 @@ describe TodoService, services: true do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
+ let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
+ let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
@@ -145,6 +147,14 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
+
+ it 'does not create todo when leaving a note on commit' do
+ should_not_create_any_todo { service.new_note(note_on_commit, john_doe) }
+ end
+
+ it 'does not create todo when leaving a note on snippet' do
+ should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
+ end
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index a698f484df1..65d59e6813c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,7 +7,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
Capybara.default_wait_time = timeout
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
new file mode 100644
index 00000000000..05a76ee4bdb
--- /dev/null
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe 'devise/shared/_signin_box' do
+ describe 'Crowd form' do
+ before do
+ stub_devise
+ assign(:ldap_servers, [])
+ end
+
+ it 'is shown when Crowd is enabled' do
+ enable_crowd
+
+ render
+
+ expect(rendered).to have_selector('#tab-crowd form')
+ end
+
+ it 'is not shown when Crowd is disabled' do
+ render
+
+ expect(rendered).not_to have_selector('#tab-crowd')
+ end
+ end
+
+ def stub_devise
+ allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
+ allow(view).to receive(:resource).and_return(spy)
+ allow(view).to receive(:resource_name).and_return(:user)
+ end
+
+ def enable_crowd
+ allow(view).to receive(:form_based_providers).and_return([:crowd])
+ allow(view).to receive(:crowd_enabled?).and_return(true)
+ allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
+ and_return('/crowd')
+ end
+end
diff --git a/vendor/assets/javascripts/cropper.js b/vendor/assets/javascripts/cropper.js
new file mode 100755
index 00000000000..84aa6119ec3
--- /dev/null
+++ b/vendor/assets/javascripts/cropper.js
@@ -0,0 +1,2972 @@
+/*!
+ * 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
new file mode 100755
index 00000000000..41ee4bd546c
--- /dev/null
+++ b/vendor/assets/stylesheets/cropper.css
@@ -0,0 +1,379 @@
+/*!
+ * 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;
+}